Sección 4 Python

Python es un lenguaje de programación de alto nivel, interactivo e interpretado. Es de código abierto, multi-plataforma y se adecua a diversos paradigmas de programación como lo es la programación orientada a objetos.

El intérprete de Python y su amplia gama de bibliotecas estándar están disponibles de forma gratuita para la mayoría de plataformas en el sitio web oficial de Python. Además, este sitio proporciona documentación adicional, programas y herramientas que complementan su ecosistema.

  • Características de Python:

    • Sintaxis muy clara y legible.

    • Fuerte capacidad de introspección.

    • Orientación a objetos intuitiva.

    • Altamente modular, soporta paquetes jerárquicos.

    • Enfocado en el uso de excepciones para el manejo de errores.

    • Tipos de datos dinámicos de muy alto nivel.

    • Extensa biblioteca estándar (STL) y módulos de terceros para prácticamente todas las tareas.

    • Extensiones y módulos fácilmente escritos en C, C + + (o Java para Jython, o. NET para IronPython).

    • Integrable dentro de las aplicaciones como una interfaz de scripting.

  • Aplicaciones de Python: se ha utilizado para desarrollar:

    • Aplicaciones de escritorio.

    • Aplicaciones web.

    • Análisis de datos.

    • Administración de servidores.

    • Seguridad y análisis de penetración.

    • Cómputo en la nube.

  • Cómputo científico.

  • Análisis de lenguaje natural.

  • Visión artificial.

  • Animación, videojuegos e imágenes generadas por computadora.

  • Aplicaciones móviles.

4.1 Breve introducción a los lenguajes de programación.

Un lenguaje se define como un conjunto de secuencias de símbolos que posibilitan la creación y transmisión de mensajes entre un emisor y un receptor. Aunque la naturaleza exhibe ciertos tipos de lenguajes, los seres humanos han desarrollado una variedad de sistemas lingüísticos de gran complejidad.

Los lenguajes se componen principalmente de dos aspectos: la gramática, que aborda la estructura del lenguaje, y la semántica, que se ocupa del significado del lenguaje.

La gramática, a su vez, incluye:

  • Morfología: relacionada con la construcción de las unidades lingüísticas (género, tiempos verbales, declinaciones).
  • Sintaxis: referente a la manera en que se deben ordenar y estructurar las unidades lingüísticas en las expresiones.

En el transcurso del siglo XX, figuras como Alan Turing y Alonzo Church sentaron los fundamentos del cálculo, la programación y sus respectivos lenguajes. Los lenguajes de programación contemporáneos, a diferencia de los lenguajes naturales, presentan una morfología estructurada y simplificada diseñada para ejecutar instrucciones precisas en los sistemas informáticos.

  • Lenguajes de alto y bajo nivel:

Los lenguajes de bajo nivel, como el lenguaje ensamblador, consisten en un conjunto elemental de instrucciones que son ejecutadas directamente por la unidad de procesamiento de un sistema informático. Estos lenguajes están estrechamente vinculados al tipo de procesador que los procesa y suelen ser difíciles de elaborar e interpretar para las personas.

Por otro lado, los lenguajes de alto nivel son más comprensibles para los humanos y, en general, menos dependientes del hardware específico, aunque requieren ser traducidos a lenguaje de bajo nivel para su ejecución.

  • Lenguajes compilados e interpretados:

Los lenguajes de alto nivel se comunican con los sistemas informáticos de dos maneras distintas:

  1. A través de un compilador, que traduce el código del programa a lenguaje de bajo nivel, generando un “archivo binario” que puede ser ejecutado posteriormente.

  2. Mediante un intérprete, que ejecuta las instrucciones ingresadas de inmediato.

Por lo general, los lenguajes compilados son más eficientes en términos de velocidad y consumo de recursos que los lenguajes interpretados, ya que el archivo resultante es código de bajo nivel. En contraste, los lenguajes interpretados requieren un proceso adicional a través de varios niveles de abstracción antes de que las instrucciones sean ejecutadas por el sistema.

Python es un lenguaje interpretado de alto nivel.

4.2 Entornos interactivos

Debido a que Python es un lenguaje interpretado, es posible utilizarlo mediante un entorno interactivo (shell) o mediante el uso de scripts.

El shell interactivo de Python funciona a través de una terminal.

Para sistemas basados en UNIX, como GNU/Linux, y Mac OS X, es requerido abrir una terminal y llamar al shell de la siguiente manera:

python

Dependiendo de la versión o la forma en que fue instalado, a veces se manda a llamar con la siguiente forma:

python3

Para salir del entorno interactivo usamos exit()

Para desplegar el texto Hola Mundo desde en entorno interactivo sólo es necesario teclear lo siguiente:

print("Hola, Mundo.")
## Hola, Mundo.

4.3 Palabras reservadas de Python.

Las palabras reservadas, también conocidas como keywords, son los nombres predeterminados que el intérprete de Python proporciona de forma inherente. No se recomienda emplear estas palabras para asignar nombres a otros objetos.

Para acceder al listado completo de palabras reservadas, se puede consultar ingresando help(‘keywords’) desde la interfaz interactiva de Python.

help("keywords")
## 
## Here is a list of the Python keywords.  Enter any keyword to get more help.
## 
## False               class               from                or
## None                continue            global              pass
## True                def                 if                  raise
## and                 del                 import              return
## as                  elif                in                  try
## assert              else                is                  while
## async               except              lambda              with
## await               finally             nonlocal            yield
## break               for                 not

4.4 Sintaxis de nombres en Python

En Python 3, la sintaxis para la elaboración de nombres sigue ciertas reglas y convenciones que son importantes para escribir un código claro y legible. Aquí hay algunas pautas clave:

  • Caracteres permitidos: Los nombres pueden incluir letras minúsculas (a-z), letras mayúsculas (A-Z), dígitos (0-9) y el guion bajo (_). No pueden comenzar con un número.

  • Convenciones de estilo: Se recomienda seguir las convenciones de estilo de Python, que generalmente sugieren el uso de minúsculas para los nombres de variables y funciones, y mayúsculas para constantes. Para nombres compuestos, se recomienda separar las palabras con guiones bajos (snake_case).

  • Palabras reservadas: No se pueden utilizar palabras reservadas de Python como nombres de variables, ya que estas palabras tienen significados específicos en el lenguaje y están reservadas para su uso en la sintaxis de Python.

  • Significado claro: Es importante elegir nombres que proporcionen un significado claro y descriptivo sobre el propósito de la variable, función o clase que están representando. Esto ayuda a que el código sea más legible y comprensible para otros programadores.

  • Evitar nombres genéricos: Trate de evitar nombres genéricos como “a”, “b”, “x”, etc. Utilice nombres que reflejen el propósito y la función de la variable o función.

4.5 Tipos de datos

En Python, los tipos de datos son la base de cualquier programa. Python es un lenguaje de programación dinámico, por lo que no es necesario declarar explícitamente el tipo de datos cuando se crea una variable. A continuación, exploraremos los tipos de datos básicos en Python.

Tipo de dato Descripción Ejemplo Colección Indexable Mutable
int Números enteros 5, -10, 100 No No No
float Números de punto flotante 3.14, -0.001, 2.0 No No No
str Cadena de caracteres ‘Hola’, “Mundo” No No
bool Booleano (Verdadero o Falso) True, False No No
list Lista ordenada de elementos [1, 2, 3], [‘a’, ‘b’, ‘c’]
tuple Secuencia inmutable de elementos (1, 2, 3), (‘a’, ‘b’, ‘c’) No
dict Colección de pares clave-valor {‘nombre’: ‘Juan’, ‘edad’: 30} No
set Colección no ordenada de elementos únicos {1, 2, 3}, {‘a’, ‘b’, ‘c’} No

Las colecciones son estructuras que almacenan una serie de elementos. Cada uno de estos elementos se puede considerar como una entidad independiente dentro de la colección.

Los tipos indexables permiten acceder a cada elemento dentro de la colección mediante un identificador único, que puede ser un número entero (índice) o una clave, dependiendo del tipo de colección.

Los tipos mutables tienen la característica de permitir la modificación de su contenido. Esto incluye la capacidad de agregar, eliminar o modificar elementos existentes en la colección.

4.5.1 Números

En Python, hay tres tipos principales de números: enteros (int), números de punto flotante (float) y números complejos (complex). Veamos ejemplos de cada uno:

# Entero
numero_entero = 10
print(numero_entero)
## 10
# Punto flotante
numero_flotante = 3.14
print(numero_flotante)
## 3.14
# Complejo
numero_complejo = 2 + 3j
print(numero_complejo)
## (2+3j)

La precisión de los números flotantes debe ser considerada cuidadosamente, ya que está influenciada significativamente por la capacidad del equipo de cómputo. En ciertas ocasiones, las operaciones con números de tipo float pueden arrojar aproximaciones en lugar de resultados exactos. Es importante tener en cuenta estas limitaciones al trabajar con números flotantes.

2 / 3
## 0.6666666666666666

Python permite realizar operaciones básicas con los tipos de datos mencionados anteriormente. A continuación, se muestran algunos ejemplos:

# Operaciones numéricas
resultado = 10 + 5
diferencia = 10 - 5
producto = 10 * 5
cociente = 10 / 5
modulo = 10 % 3
potencia = 10 ** 2

4.5.2 Cadenas de Caracteres

Las cadenas de caracteres (str) se utilizan para representar texto en Python. Se pueden definir utilizando comillas simples (’) o dobles (“):

cadena_simple = 'Hola, mundo!'
print(cadena_simple)
## Hola, mundo!
cadena_doble = "¡Hola, Python!"
print(cadena_doble)
## ¡Hola, Python!

En Python podemos concatenar cadenas de caracteres al sumarlas:

# Concatenación de cadenas
saludo = "Hola" + " " + "Mundo"
print(saludo)
## Hola Mundo

La indexación en Python te permite acceder a elementos individuales dentro de una secuencia, como una cadena de caracteres (string) o una lista. Los índices se utilizan para identificar la posición de un elemento en la secuencia.

  • Índices positivos: Comienzan desde 0 y avanzan hacia la derecha.
  • Índices negativos: Comienzan desde -1 y avanzan hacia la izquierda.
a = "hola"
print(a[0])  # Salida: 'h'
## h
print(a[1])  # Salida: 'o'
## o
print(a[-1]) # Salida: 'a'
## a

El rebanado (slicing) te permite extraer subconjuntos de elementos de una secuencia, como una cadena de caracteres o una lista. La sintaxis general para el rebanado es inicio:fin:paso, donde:

  • inicio: Índice desde el cual comienza la rebanada.

  • fin: Índice hasta el cual se incluyen elementos en la rebanada, pero no incluyendo el elemento en este índice.

  • paso: Tamaño del paso o incremento entre los elementos seleccionados.

a = "hola, mundooo!"
print(a[3:6])   
## a,
print(a[2:10:2]) 
## l,mn
print(a[::3])    
## hamdo

Las cadenas de caracteres son inmutables en Python, lo que significa que no se pueden modificar directamente. Sin embargo, puedes crear nuevas cadenas basadas en las originales utilizando métodos como replace().

a = "hola, clase!"
nueva_cadena = a.replace('l', 'z', 1) 
print(nueva_cadena) 
## hoza, clase!
a = "hola, clase!"
nueva_cadena = a.replace('l', 'z')      # Reemplaza todas las 'l' con 'z'
print(nueva_cadena) 
## hoza, czase!

Las cadenas de caracteres en Python tienen una variedad de métodos útiles que puedes utilizar para manipular y trabajar con ellas. Puedes acceder a estos métodos utilizando la notación de punto después de una cadena de caracteres seguida por el método que deseas utilizar.

El formateo de cadenas en Python te permite crear cadenas complejas combinando texto y valores de variables de una manera legible y eficiente. Hay varias formas de formatear cadenas en Python, incluidas las expresiones de formato, el método format(), y el formateo de cadenas con el operador %.

El operador % se utiliza para insertar valores en una cadena utilizando un formato específico.

  • Especificadores de Formato:

    • %d: Entero decimal.

    • %f: Número de punto flotante.

    • %s: Cadena de caracteres.

# Ejemplo de formateo de cadena con el operador %
cadena = 'Un entero: %i; un flotante: %f; otra cadena: %s' % (1, 0.1, 'cadena')
print(cadena)
## Un entero: 1; un flotante: 0.100000; otra cadena: cadena
# Formateo de nombres de archivos
i = 102
nombre_archivo = 'procesamiento_del_dataset_%d.txt' % i
print(nombre_archivo) 
## procesamiento_del_dataset_102.txt

En el primer ejemplo, se insertan un entero, un flotante y una cadena en la cadena de formato utilizando el operador %. En el segundo ejemplo, se utiliza el operador % para insertar un entero en el nombre del archivo.

Cuando se insertan múltiples valores, estos deben estar dentro de una tupla ( ) después del operador %. Si solo se inserta un valor, no es necesario el uso de tuplas después del operador %. El operador % es útil pero está siendo reemplazado gradualmente por el método format().

El método format() es una técnica más moderna y flexible para formatear cadenas en comparación con el uso del operador %. Aunque el operador % todavía es válido y funcional, se recomienda utilizar el método format() debido a su mayor versatilidad y legibilidad.

El método format() permite insertar valores en una cadena de forma más explícita y flexible. Utiliza llaves {} como marcadores de posición en la cadena, y luego proporciona los valores que se insertarán en esos marcadores de posición. Por ejemplo:

# Ejemplo básico
cadena = "Hola, {}! Hoy es {}."
mensaje = cadena.format("Juan", "miércoles")
print(mensaje)
## Hola, Juan! Hoy es miércoles.
# Especificando el orden de los argumentos
cadena = "Hola, {1}! Hoy es {0}."
mensaje = cadena.format("lunes", "María")
print(mensaje)  
## Hola, María! Hoy es lunes.
# Especificando nombres de argumentos
cadena = "Hola, {nombre}! Hoy es {dia}."
mensaje = cadena.format(nombre="Pedro", dia="jueves")
print(mensaje)  
## Hola, Pedro! Hoy es jueves.

Algunas ventajas de format() sobre %:

  1. Legibilidad mejorada: El método format() ofrece una sintaxis más clara y legible en comparación con el operador %.

  2. Mayor flexibilidad: format() permite especificar el orden de los argumentos y asignar nombres a los argumentos, lo que lo hace más flexible y fácil de mantener en comparación con el operador %.

  3. Compatible con objetos de diferentes tipos: format() es compatible con una amplia gama de tipos de datos y objetos, lo que lo hace más versátil en comparación con el operador %.

4.5.3 Listas

Una lista (list) en Python es una secuencia ordenada de elementos que pueden ser de diferentes tipos. Se definen utilizando corchetes []:

mi_lista = [1, 2, 3, 'cuatro', 5.0]
print(mi_lista)
## [1, 2, 3, 'cuatro', 5.0]

Para acceder a los elementos de una lista, podemos usar su índice. Los índices en Python comienzan desde 0 para el primer elemento y van incrementando de uno en uno.

mi_lista = [1, 2, 3, 'cuatro', 5.0]
primer_elemento = mi_lista[0]
print(primer_elemento)
## 1
print(mi_lista[0])  # Imprime el primer elemento: 1
## 1
print(mi_lista[3])  # Imprime 'cuatro'
## cuatro

También se puede acceder a los elementos de la lista utilizando índices negativos, los cuales cuentan desde el final de la lista hacia el principio.

print(mi_lista[-1])  # Imprime el último elemento: 5.0
## 5.0
print(mi_lista[-2])  # Imprime 'cuatro'
## cuatro

Puedes añadir elementos a una lista utilizando el método append() para agregar un elemento al final de la lista.

mi_lista.append(6)
print(mi_lista)  
## [1, 2, 3, 'cuatro', 5.0, 6]

También puedes extender una lista añadiendo todos los elementos de otra lista utilizando el método extend().

mi_lista.extend([7, 8, 9])
print(mi_lista)  
## [1, 2, 3, 'cuatro', 5.0, 6, 7, 8, 9]

Puedes eliminar elementos de una lista utilizando la palabra clave del seguida del índice del elemento que deseas eliminar.

del mi_lista[3]  # Elimina el elemento 'cuatro'
print(mi_lista) 
## [1, 2, 3, 5.0, 6, 7, 8, 9]

También puedes utilizar el método remove() para eliminar un elemento específico por su valor.

mi_lista.remove(3)  # Elimina el elemento 3
print(mi_lista)  
## [1, 2, 5.0, 6, 7, 8, 9]

Puedes unir dos listas utilizando el operador + o el método extend().

lista1 = [1, 2, 3]
lista2 = [4, 5, 6]

nueva_lista = lista1 + lista2
print(nueva_lista)  
## [1, 2, 3, 4, 5, 6]
lista1.extend(lista2)
print(lista1)  
## [1, 2, 3, 4, 5, 6]

Otra forma de operar listas es la siguiente. La repetición de listas implica crear una nueva lista duplicando los elementos de una lista existente un número determinado de veces. Puedes lograr esto utilizando el operador de multiplicación. Por ejemplo:

colores = ['rojo', 'verde', 'azul']
colores_repetidos = colores * 3
print(colores_repetidos) 
## ['rojo', 'verde', 'azul', 'rojo', 'verde', 'azul', 'rojo', 'verde', 'azul']

El método sort() ordena los elementos de la lista en orden ascendente de forma predeterminada. Sin embargo, también acepta argumentos opcionales que te permiten personalizar el ordenamiento según tus necesidades. Por ejemplo, para ordenarlos ascendente:

# Definir una lista desordenada de colores
colores = ['rojo', 'verde', 'azul', 'amarillo', 'naranja', 'morado']

# Ordenar la lista de colores en orden ascendente
colores.sort()

# Imprimir la lista ordenada
print(colores)
## ['amarillo', 'azul', 'morado', 'naranja', 'rojo', 'verde']

Para ordenarlas de forma descendente:

# Definir una lista desordenada de colores
colores = ['rojo', 'verde', 'azul', 'amarillo', 'naranja', 'morado']

# Ordenar la lista de colores en orden descendente
colores.sort(reverse=True)

# Imprimir la lista ordenada
print(colores)
## ['verde', 'rojo', 'naranja', 'morado', 'azul', 'amarillo']

Algunas notas a considerar:

  • El método sort() modifica la lista original y no devuelve ningún valor.

  • Si los elementos de la lista son de tipos diferentes, la función sort() generará un error de tipo.

  • Si deseas ordenar una lista sin modificar la original, puedes usar la función sorted(), que devuelve una nueva lista ordenada sin modificar la lista original.

Otra forma de acceder a los datos de las listas en Python es usando el método de “rebanada” (slice): lista[inicio:fin]. Esta rebanada incluye los elementos cuyos índices están en el rango desde inicio hasta fin - 1:

  • inicio: Es el índice desde el cual comenzamos a incluir elementos en la rebanada.

  • fin: Es el índice hasta el cual incluimos elementos en la rebanada, pero no incluyendo el elemento en este índice.

Supongamos que tenemos una lista llamada colores:

colores = ['rojo', 'verde', 'azul', 'amarillo', 'naranja', 'morado']

Por ejemplo, si queremos obtener una rebanada de colores desde el índice 1 hasta el índice 4, usaríamos la notación colores[1:4] y vamos a obtener una rebanada que incluye elementos desde el índice 1 (inclusive) hasta el índice 4 (no inclusive).

colores[1:4]
## ['verde', 'azul', 'amarillo']

Si ejecutamos colores[2:5], obtendremos una rebanada que incluye elementos desde el índice 2 (inclusive) hasta el índice 5 (no inclusive). La rebanada contendrá los elementos 'azul', 'amarillo' y 'naranja'.

print(colores[2:5])  
## ['azul', 'amarillo', 'naranja']

La longitud de la rebanada colores[inicio:fin] es fin - inicio. Es decir, el número de elementos en la rebanada es igual a la diferencia entre el índice de parada y el índice de inicio.

Por ejemplo, colores[1:4] contiene los elementos desde el índice 1 hasta el índice 3 (no incluido), lo que hace que tenga una longitud de 4 - 1 = 3.

Esta es una manera flexible y poderosa de acceder a subconjuntos de elementos en una lista en Python.

Otra sintaxis de “rebanadas” en Python con la notación colores[inicio:fin:salto] se refiere a la técnica para seleccionar un subconjunto de elementos de una lista (colors) utilizando tres parámetros:

  • inicio: Indica el índice desde el cual comenzamos a incluir elementos en la rebanada.

  • fin: Indica el índice hasta el cual incluimos elementos en la rebanada, sin incluir el elemento en este índice.

  • salto: Indica el tamaño del paso o incremento entre los elementos seleccionados. Es decir, cuántos elementos saltamos en cada paso.

Si ejecutamos colores[::2], obtenemos una rebanada que incluye todos los elementos de la lista colores, pero seleccionando cada segundo elemento. Es como si empezáramos desde el principio de la lista, terminando al final y seleccionando cada segundo elemento.

print(colores[::2])
## ['rojo', 'azul', 'naranja']

Si ejecutamos colores[1:5:2], obtenemos una rebanada que incluye elementos desde el índice 1 (inclusive) hasta el índice 5 (no inclusive), seleccionando cada segundo elemento en ese rango.

print(colores[1:5:2])
## ['verde', 'amarillo']

También es posible usar un valor de salto negativo, lo que significa que los elementos se seleccionan en orden inverso. Por ejemplo, colores[::-1] devuelve una rebanada que incluye todos los elementos de colores, pero en orden inverso.

print(colores[::-1])
## ['morado', 'naranja', 'amarillo', 'azul', 'verde', 'rojo']

Ejercicios:

  1. Dada una lista de números, intercambia cada par de elementos adyacentes. Por ejemplo, si la lista es [1, 2, 3, 4, 5], el resultado sería [2, 1, 4, 3, 5].

  2. Dada una lista de letras, invierte el orden de los elementos en grupos de tres. Por ejemplo, si la lista es [‘a’, ‘b’, ‘c’, ‘d’, ‘e’, ‘f’, ‘g’, ‘h’, ‘i’], el resultado sería [‘c’, ‘b’, ‘a’, ‘f’, ‘e’, ‘d’, ‘i’, ‘h’, ‘g’].

Soluciones: Link

Cuando necesitas realizar una copia de una lista en Python, es importante entender que simplemente hacer colores2 = colores no crea una copia real de la lista. Ambas variables, colores2 y colores, apuntarán a la misma lista en la memoria. Esto significa que cualquier modificación realizada en una de las variables afectará a la otra. Para crear una copia real de la lista, puedes usar el método list() o la técnica de rebanada.

  • Usando el método list():
colores2 = list(colores)  # Crear una copia de colores en una nueva lista colores2
  • Usando rebanadas:
rcolores2 = colores[:]  # Crear una copia de colores en una nueva lista rcolores2

En Python, puedes revertir el orden de los elementos en una lista utilizando el método reverse().

  • Uso del método reverse():
rcolores2.reverse()  # Revertir el orden de los elementos en la lista rcolores2

Esto modificará la lista original en su lugar y no devolverá ninguna nueva lista.

# Definir una lista de colores
colores = ['rojo', 'verde', 'azul', 'amarillo', 'naranja', 'morado']

# Crear una copia de colors en una nueva lista rcolors2
rcolores2 = list(colores)

# Revertir el orden de los elementos en la lista rcolors2
rcolores2.reverse()

# Imprimir la lista original y la lista revertida
print("Lista original:", colores)
## Lista original: ['rojo', 'verde', 'azul', 'amarillo', 'naranja', 'morado']
print("Lista revertida:", rcolores2)
## Lista revertida: ['morado', 'naranja', 'amarillo', 'azul', 'verde', 'rojo']

4.5.4 Tuplas

Las tuplas (tuple) son secuencias ordenadas similares a las listas, pero son inmutables, es decir, no se pueden modificar después de su creación. Se definen utilizando paréntesis ():

mi_tupla = (1, 2, 3, 'cuatro', 5.0)
print(mi_tupla)
## (1, 2, 3, 'cuatro', 5.0)

Para acceder a los elementos de una tupla, podemos utilizar su índice, al igual que en las listas.

ultimo_elemento = mi_tupla[-1]
print(ultimo_elemento)
## 5.0
print(mi_tupla[0])  # Imprime el primer elemento: 1
## 1
print(mi_tupla[3])  # Imprime 'cuatro'
## cuatro

También se puede acceder a los elementos de la tupla utilizando índices negativos, que cuentan desde el final de la tupla hacia el principio.

print(mi_tupla[-1])  # Imprime el último elemento: 5.0
## 5.0
print(mi_tupla[-2])  # Imprime 'cuatro'
## cuatro

Dado que las tuplas son inmutables, no se pueden borrar elementos individualmente ni modificar la tupla después de su creación. Aunque las tuplas son inmutables, todavía tienen algunas características útiles en Python:

  • Empaquetado y desempaquetado de tuplas: Puedes empaquetar múltiples valores en una sola tupla y desempaquetarlos en variables individuales.
tupla = 1, 2, 3  # Empaquetado de valores
a, b, c = tupla  # Desempaquetado en variables individuales
print(a, b, c)   # Imprime 1 2 3
## 1 2 3

El empaquetado y desempaquetado de tuplas son conceptos importantes en Python que permiten trabajar con múltiples valores de manera eficiente.

El empaquetado de tuplas es el proceso de agrupar múltiples valores en una sola tupla. Esto se hace simplemente colocando los valores separados por comas, sin necesidad de utilizar paréntesis, por ejemplo:

tupla_empaquetada = 1, 2, 3

Aquí, hemos empaquetado los valores 1, 2 y 3 en una sola tupla llamada tupla_empaquetada.

El desempaquetado de tuplas es el proceso inverso al empaquetado. Permite extraer los valores individuales de una tupla y asignarlos a variables individuales. Esto se hace asignando la tupla a la derecha del signo de igualdad y las variables a la izquierda, por ejemplo:

a, b, c = tupla_empaquetada

Aquí, estamos desempaquetando la tupla tupla_empaquetada y asignando sus valores a las variables a, b y c respectivamente. Algunas de las ventajas del empaquetado y desempaquetado de tuplas son las siguientes:

  1. Sintaxis concisa: El empaquetado y desempaquetado de tuplas permite escribir código de manera más concisa y legible.

  2. Asignación múltiple: Permite asignar valores a múltiples variables en una sola línea de código.

Los usos más comunes que tienen son los siguientes:

  1. Intercambio de valores: Se pueden intercambiar los valores de dos variables sin necesidad de una variable temporal utilizando el desempaquetado de tuplas.
a, b = b, a
  1. Retorno múltiple de funciones: Las funciones pueden devolver múltiples valores empaquetados en una tupla, y luego estos valores pueden ser desempaquetados cuando se llama a la función.
def obtener_coordenadas():
    return 10, 20

x, y = obtener_coordenadas()
print("Coordenadas:", x, y)
## Coordenadas: 10 20

El empaquetado y desempaquetado de tuplas son herramientas poderosas que permiten trabajar con múltiples valores de manera eficiente en Python.

  • Tuplas como claves de diccionario: Dado que las tuplas son inmutables, pueden ser utilizadas como claves en un diccionario de Python.
diccionario = {(1, 2): 'valor', (3, 4): 'otro valor'}
print(diccionario[(1, 2)])  # Imprime 'valor'
## valor

Aunque las tuplas son más simples que las listas, su inmutabilidad las hace útiles en situaciones donde no deseas que los datos cambien después de su definición.

4.5.5 Conjuntos

Los conjuntos (set) son colecciones desordenadas de elementos únicos. Se definen utilizando llaves {}:

mi_conjunto = {1, 2, 3, 4, 5}
print(mi_conjunto)
## {1, 2, 3, 4, 5}

Los conjuntos en Python son mutables, lo que significa que puedes agregar y eliminar elementos, pero no puedes modificar los elementos existentes directamente. Para agregar elementos a un conjunto, puedes utilizar el método add().

mi_conjunto.add(6)
print(mi_conjunto)  
## {1, 2, 3, 4, 5, 6}

Para eliminar un elemento de un conjunto, puedes usar el método remove() o discard(). La diferencia entre estos dos métodos radica en cómo manejan la eliminación de un elemento que no está presente en el conjunto. remove() generará un error, mientras que discard() no.

mi_conjunto.remove(3)
print(mi_conjunto)
## {1, 2, 4, 5, 6}
mi_conjunto.discard(5)
print(mi_conjunto)
## {1, 2, 4, 6}

Los conjuntos en Python admiten operaciones comunes de teoría de conjuntos como unión, intersección y diferencia.

  • Unión: Combina todos los elementos únicos de dos conjuntos.
conjunto1 = {1, 2, 3}
conjunto2 = {3, 4, 5}

union = conjunto1.union(conjunto2)
print(union)
## {1, 2, 3, 4, 5}
  • Intersección: Devuelve un conjunto que contiene los elementos que son comunes a ambos conjuntos.
interseccion = conjunto1.intersection(conjunto2)
print(interseccion)  
## {3}
  • Diferencia: Devuelve un conjunto que contiene los elementos que están en el primer conjunto pero no en el segundo.
diferencia = conjunto1.difference(conjunto2)
print(diferencia) 
## {1, 2}

Los conjuntos son útiles cuando necesitas almacenar una colección de elementos únicos y realizar operaciones de conjuntos eficientes como la eliminación de duplicados y la comparación de colecciones.

En resumen, los conjuntos en Python proporcionan una manera eficiente de trabajar con colecciones de elementos únicos y son útiles para una variedad de tareas, como la eliminación de duplicadosy la realización de operaciones de conjuntos.

4.5.6 Diccionarios

Los diccionarios (dict) son colecciones de pares clave-valor. Cada elemento del diccionario tiene una clave y un valor asociado. Se definen utilizando llaves {} y separando las claves y los valores con ::

mi_diccionario = {'nombre': 'Robeto', 'edad': 20, 'ciudad': 'Morelia'}
print(mi_diccionario)
## {'nombre': 'Robeto', 'edad': 20, 'ciudad': 'Morelia'}

Los diccionarios en Python son mutables, lo que significa que puedes agregar, modificar y eliminar elementos según sea necesario.

Puedes acceder a los valores del diccionario utilizando sus claves.

print(mi_diccionario['nombre'])  # Imprime 'Roberto'
## Robeto
print(mi_diccionario['edad'])    # Imprime 20
## 20

Si intentas acceder a una clave que no existe en el diccionario, Python generará un error KeyError.

Puedes agregar un nuevo par clave-valor al diccionario simplemente asignando un valor a una nueva clave.

mi_diccionario['email'] = 'roberto@example.com'
print(mi_diccionario)
## {'nombre': 'Robeto', 'edad': 20, 'ciudad': 'Morelia', 'email': 'roberto@example.com'}

Puedes modificar el valor asociado a una clave existente del diccionario.

mi_diccionario['edad'] = 21
print(mi_diccionario)
## {'nombre': 'Robeto', 'edad': 21, 'ciudad': 'Morelia', 'email': 'roberto@example.com'}

Puedes eliminar un elemento del diccionario utilizando la palabra clave del.

del mi_diccionario['ciudad']
print(mi_diccionario)
## {'nombre': 'Robeto', 'edad': 21, 'email': 'roberto@example.com'}

También puedes utilizar el método pop() para eliminar un elemento y devolver su valor.

valor_eliminado = mi_diccionario.pop('edad')
print(valor_eliminado)   # Imprime 21
## 21
print(mi_diccionario)
## {'nombre': 'Robeto', 'email': 'roberto@example.com'}

Algunos conceptos clave de los diccionarios son los siguientes:

  • keys(), values(), items(): Estos métodos devuelven vistas de las claves, valores y pares clave-valor del diccionario, respectivamente.

    1. Ejemplo de keys():
mi_diccionario = {'nombre': 'Juan', 'edad': 30, 'ciudad': 'Madrid'}
claves = mi_diccionario.keys()
print(claves)  
## dict_keys(['nombre', 'edad', 'ciudad'])
# Iterar sobre las claves
for clave in claves:
    print(clave)
## nombre
## edad
## ciudad
  1. Ejemplo de values():
mi_diccionario = {'nombre': 'Juan', 'edad': 30, 'ciudad': 'Madrid'}
valores = mi_diccionario.values()
print(valores)
## dict_values(['Juan', 30, 'Madrid'])
# Iterar sobre los valores
for valor in valores:
    print(valor)  
## Juan
## 30
## Madrid
  1. Ejemplo de items():
mi_diccionario = {'nombre': 'Juan', 'edad': 30, 'ciudad': 'Madrid'}
items = mi_diccionario.items()
print(items) 
## dict_items([('nombre', 'Juan'), ('edad', 30), ('ciudad', 'Madrid')])
# Iterar sobre los pares clave-valor
for clave, valor in items:
    print(clave, valor) 
## nombre Juan
## edad 30
## ciudad Madrid
  • update(): Este método agrega los elementos de un diccionario a otro diccionario existente.
diccionario1 = {'a': 1, 'b': 2}
diccionario2 = {'b': 3, 'c': 4}

diccionario1.update(diccionario2)
print(diccionario1)  # Salida: {'a': 1, 'b': 3, 'c': 4}
## {'a': 1, 'b': 3, 'c': 4}
  • clear(): Este método elimina todos los elementos del diccionario.
mi_diccionario = {'nombre': 'Juan', 'edad': 30, 'ciudad': 'Madrid'}
mi_diccionario.clear()
print(mi_diccionario)  # Salida: {}
## {}

Estos métodos son muy útiles para trabajar con diccionarios en Python y permiten manipular fácilmente las claves, valores y pares clave-valor, así como agregar, eliminar y actualizar elementos en los diccionarios de manera eficiente.

Supongamos que queremos agregar nuevas entradas a nuestro diccionario, para eso usamos el método append.

mi_diccionario = {
    'nombre': ['Roberta', 'María', 'Pedro'],
    'edad': [30, 25, 35],
    'ciudad': ['Madrid', 'Barcelona', 'Sevilla']
}

mi_diccionario['nombre'].append('Luis')

O podríamos definir las variables a agregar y usar el método append.

mi_diccionario = {
    'nombre': ['Roberta', 'María', 'Pedro'],
    'edad': [30, 25, 35],
    'ciudad': ['Madrid', 'Barcelona', 'Sevilla']
}

# Agregar un nuevo nombre, edad y ciudad
nuevo_nombre = 'Luis'
nueva_edad = 40
nueva_ciudad = 'Valencia'

mi_diccionario['nombre'].append(nuevo_nombre)
mi_diccionario['edad'].append(nueva_edad)
mi_diccionario['ciudad'].append(nueva_ciudad)

# Imprimir el diccionario actualizado
print(mi_diccionario)
## {'nombre': ['Roberta', 'María', 'Pedro', 'Luis'], 'edad': [30, 25, 35, 40], 'ciudad': ['Madrid', 'Barcelona', 'Sevilla', 'Valencia']}

Una forma de obtener los valores de edad y ciudad para cada nombre es la siguiente.

mi_diccionario = {
    'nombre': ['Roberta', 'María', 'Pedro'],
    'edad': [30, 25, 35],
    'ciudad': ['Madrid', 'Barcelona', 'Sevilla']
}

# Recuperar listas de nombres y edades
nombres = mi_diccionario['nombre']
edades = mi_diccionario['edad']

# Buscar la posición de María y Pedro en la lista de nombres
pos_maria = nombres.index('María')
pos_pedro = nombres.index('Pedro')

# Obtener las edades de María y Pedro usando las posiciones encontradas
edad_maria = edades[pos_maria]
edad_pedro = edades[pos_pedro]

# Imprimir las edades de María y Pedro
print("Edad de María:", edad_maria)
## Edad de María: 25
print("Edad de Pedro:", edad_pedro)
## Edad de Pedro: 35

De una forma más directa, podemos obtener los valores como sigue:

mi_diccionario = {
    'nombre': ['Juan', 'María', 'Pedro'],
    'edad': [30, 25, 35],
    'ciudad': ['Madrid', 'Barcelona', 'Sevilla']
}

# Obtener las edades de María y Pedro directamente del diccionario
edad_maria = mi_diccionario['edad'][mi_diccionario['nombre'].index('María')]
edad_pedro = mi_diccionario['edad'][mi_diccionario['nombre'].index('Pedro')]

# Imprimir las edades de María y Pedro
print("Edad de María:", edad_maria)
## Edad de María: 25
print("Edad de Pedro:", edad_pedro)
## Edad de Pedro: 35

Los diccionarios en Python son estructuras de datos muy versátiles y se utilizan ampliamente para mapear claves a valores y para almacenar datos de manera eficiente. Son útiles para muchas tareas, como el almacenamiento de configuraciones, el modelado de datos y la manipulación de datos estructurados.

4.5.7 Ejercicios

  1. Crea una lista que contenga los nombres de tres amigos tuyos.

  2. Crea un diccionario que represente la información de un libro (título, autor, año de publicación).

  3. Realiza la suma de dos números y guarda el resultado en una variable.

  4. Concatena dos cadenas de caracteres y muestra el resultado.

4.6 Flujo de control

El flujo de control en Python se refiere a la secuencia en la que se ejecutan las instrucciones en un programa. Python ofrece varias estructuras de control que permiten tomar decisiones, repetir acciones y ejecutar código en función de condiciones específicas.

4.6.1 Estructuras de Control Condicionales

4.6.1.1 if Statement

El if statement permite ejecutar un bloque de código si se cumple una condición. Por ejemplo:

edad = 20
if edad >= 18:
    print("Eres mayor de edad")
## Eres mayor de edad

4.6.1.2 if-else Statement

El if-else statement ejecuta un bloque de código si se cumple una condición y otro bloque de código si la condición no se cumple. Por ejemplo:

edad = 15
if edad >= 18:
    print("Eres mayor de edad")
else:
    print("Eres menor de edad")
## Eres menor de edad

4.6.1.3 if-elif-else Statement

El if-elif-else statement permite evaluar múltiples condiciones de manera secuencial. Por ejemplo:

puntuacion = 75
if puntuacion >= 90:
    print("Excelente")
elif puntuacion >= 70:
    print("Bien")
else:
    print("Puedes mejorar")
## Bien

Ejercicios:

  1. Escribe un programa que verifique si un número es positivo, negativo o cero.

  2. Crea un programa que determine si un número es par o impar.

4.6.2 Estructuras de Control de Bucles

4.6.2.1 for Loop

El bucle for itera sobre una secuencia, como una lista o una cadena de texto. Por ejemplo:

for i in range(4):
  print(i)
## 0
## 1
## 2
## 3

También podemos iterar sobre listas de caracteres.

frutas = ["manzana", "banana", "cereza"]
for fruta in frutas:
    print(fruta)
## manzana
## banana
## cereza
for palabra in ('genial', 'poderoso', 'legible'):
    print('Python es %s' % palabra)
## Python es genial
## Python es poderoso
## Python es legible

4.6.2.2 while Loop

El bucle while ejecuta un bloque de código mientras se cumpla una condición. Por ejemplo:

contador = 0
while contador < 5:
    print(contador)
    contador += 1
## 0
## 1
## 2
## 3
## 4

Ejercicios:

  1. Escribe un programa que muestre los números del 1 al 10 utilizando un bucle while.

  2. Crea un programa que calcule la suma de los números del 1 al 100 utilizando un bucle for.

4.6.3 Estructuras de Control de Interrupción

4.6.3.1 break Statement

El break statement se utiliza para salir de un bucle antes de que se haya completado. Por ejemplo:

for numero in range(10):
    if numero == 5:
        break
    print(numero)
## 0
## 1
## 2
## 3
## 4

4.6.3.2 continue Statement

El continue statement se utiliza para saltar la iteración actual de un bucle y continuar con la siguiente. Por ejemplo:

for numero in range(10):
    if numero == 5:
        continue
    print(numero)
## 0
## 1
## 2
## 3
## 4
## 6
## 7
## 8
## 9

Otro ejemplo:

numeros = [2, 0, 5, 3, 0, 7]

for numero in numeros:
    if numero == 0:
        continue
    print(f"El inverso de {numero} es {1 / numero}")
## El inverso de 2 es 0.5
## El inverso de 5 es 0.2
## El inverso de 3 es 0.3333333333333333
## El inverso de 7 es 0.14285714285714285

Ejercicios:

  1. Escribe un programa que encuentre el primer número divisible entre 7 y 5, entre 1500 y 2700.

  2. Crea un programa que imprima los números impares del 1 al 50, excepto aquellos que sean múltiplos de 3.

4.6.4 Expresiones condicionales

Algunas expresiones condicionales que podemos usar en las estructuras de control son las siguientes:

  • a == b: para comprobar igualdad entre dos objetos. Por ejemplo:
1==1
## True
  • a is b: para comprobar identidad entre ambos lados. Por ejemplo:
1 is 1
## True
a = 1
b = 1
a is b
## True
  • a in b: Para ver si la colección b contiene a a. Por ejemplo:
b = [1, 2, 3]
2 in b
## True
5 in b
## False

En el caso de que b sea un diccionario, esto nos verifica si a es una llave de b.

# Diccionario de nombres y edades
b = {'Juan': 30, 'María': 25, 'Pedro': 35}

# Verificamos si 'Juan' es una llave en el diccionario 'b'
if 'Juan' in b:
    print('Juan está en el diccionario.')
## Juan está en el diccionario.
# Verificamos si 'Ana' es una llave en el diccionario 'b'
if 'Ana' in b:
    print('Ana está en el diccionario.')
else:
    print('Ana no está en el diccionario.')
## Ana no está en el diccionario.

4.6.5 Iteraciones sobre secuencias

En Python, puedes iterar sobre cualquier secuencia, lo que incluye listas, tuplas, cadenas, diccionarios y conjuntos. La iteración te permite recorrer cada elemento de la secuencia uno por uno para realizar operaciones o tomar decisiones según cada elemento.

vocales = 'aeiouy'

for letra in 'poderoso':
    if letra in vocales:
        print(letra)
## o
## e
## o
## o
mensaje = "Hola ¿cómo estás?"
palabras = mensaje.split()  # retorna una lista

for palabra in palabras:
    print(palabra)
## Hola
## ¿cómo
## estás?

A veces queremos llevar un registro sobre lo que estamos iterando, una forma de hacerlo es con un for.

colores = ['rojo', 'verde', 'azul', 'amarillo']
for i in range(0, len(colores)):
    print((i, colores[i]))
## (0, 'rojo')
## (1, 'verde')
## (2, 'azul')
## (3, 'amarillo')

Otra forma de hacerlo es usando una función de python llamada enumerate. La función enumerate() en Python es una herramienta muy útil que te permite iterar sobre una secuencia mientras llevas un seguimiento del número de índice de cada elemento en la secuencia. La función enumerate() toma una secuencia (como una lista, tupla, cadena, etc.) y devuelve un objeto enumerado que genera pares de índice-elemento para cada elemento en la secuencia.

  • Utilizando enumerate() con una lista:
frutas = ['manzana', 'banana', 'cereza', 'dátil']

for indice, fruta in enumerate(frutas):
    print(f'Índice: {indice}, Fruta: {fruta}')
## Índice: 0, Fruta: manzana
## Índice: 1, Fruta: banana
## Índice: 2, Fruta: cereza
## Índice: 3, Fruta: dátil

En este ejemplo, enumerate(frutas) genera pares de índice-fruta para cada elemento en la lista frutas, y luego iteramos sobre estos pares. En cada iteración, indice contiene el índice del elemento y fruta contiene el valor del elemento.

  • Utilizando enumerate() con una cadena:
mensaje = "Python es genial"

for indice, letra in enumerate(mensaje):
    print(f'Índice: {indice}, Letra: {letra}')
## Índice: 0, Letra: P
## Índice: 1, Letra: y
## Índice: 2, Letra: t
## Índice: 3, Letra: h
## Índice: 4, Letra: o
## Índice: 5, Letra: n
## Índice: 6, Letra:  
## Índice: 7, Letra: e
## Índice: 8, Letra: s
## Índice: 9, Letra:  
## Índice: 10, Letra: g
## Índice: 11, Letra: e
## Índice: 12, Letra: n
## Índice: 13, Letra: i
## Índice: 14, Letra: a
## Índice: 15, Letra: l

Aquí, enumerate(mensaje) genera pares de índice-letra para cada letra en la cadena mensaje.

La función enumerate() es útil cuando necesitas iterar sobre una secuencia y necesitas saber tanto el índice como el valor del elemento en cada iteración. Es más legible y más eficiente que usar un contador y acceder a los elementos por índice.

4.6.6 Iteraciones sobre diccionarios

También podemos iterar sobre un diccionario en Python, tanto sobre las claves, los valores, como sobre los pares clave-valor.

  • Iterar sobre claves: Puedes iterar sobre las claves de un diccionario utilizando un bucle for. Por ejemplo:
# Diccionario
persona = {'nombre': 'Juan', 'edad': 30, 'ciudad': 'Nueva York'}

# Iterar sobre las claves
for clave in persona:
    print(clave)
## nombre
## edad
## ciudad
  • Iterar sobre valores: De igual forma, puedes iterar sobre los valores de un diccionario utilizando un bucle for:
# Iterar sobre los valores
for valor in persona.values():
    print(valor)
## Juan
## 30
## Nueva York
  • Iterar sobre pares clave-valor: Para iterar sobre las claves y valores simultáneamente, puedes usar el método items() del diccionario:
# Iterar sobre pares clave-valor
for clave, valor in persona.items():
    print(clave, valor)
## nombre Juan
## edad 30
## ciudad Nueva York

Veamos otros ejemplos. Supongamos que tenemos un diccionario representando estudiantes y sus calificaciones:

estudiantes = {'Ana': 85, 'Beto': 90, 'Carlos': 80, 'Diana': 95}

Ahora, podemos iterar sobre el diccionario para imprimir el nombre de cada estudiante junto con su calificación:

for estudiante, calificacion in estudiantes.items():
    print(f'{estudiante}: {calificacion}')
## Ana: 85
## Beto: 90
## Carlos: 80
## Diana: 95

4.6.7 Compresiones de listas

Las comprensiones de listas son una característica poderosa de Python que te permite crear listas de forma concisa y legible utilizando una sintaxis compacta. La sintaxis general de una comprensión de lista es la siguiente:

nueva_lista = [expresion for elemento in iterable if condicion]
  • expresion: Es la expresión que define cómo se va a modificar o construir cada elemento de la nueva lista.

  • elemento: Es la variable que representa cada elemento del iterable.

  • iterable: Es la secuencia o iterable sobre la cual se va a iterar.

  • condicion (opcional): Es una condición que filtra los elementos del iterable.

Veamos algunos ejemplos.

  1. Crear una lista de los cuadrados de los primeros 10 números enteros:
cuadrados = [x ** 2 for x in range(1, 11)]
print(cuadrados)
## [1, 4, 9, 16, 25, 36, 49, 64, 81, 100]
  1. Filtrar los números pares de una lista:
numeros = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
pares = [x for x in numeros if x % 2 == 0]
print(pares)
## [2, 4, 6, 8, 10]
  1. Convertir una lista de cadenas a una lista de sus longitudes:
palabras = ['python', 'es', 'genial']
longitudes = [len(palabra) for palabra in palabras]
print(longitudes)
## [6, 2, 6]

También es posible utilizar una condición else en una comprensión de lista. La sintaxis es la siguiente:

resultado = [valor_verdadero if condicion else valor_falso for elemento in iterable]

Por ejemplo:

numeros = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
resultado = ['par' if x % 2 == 0 else 'impar' for x in numeros]
print(resultado)
## ['impar', 'par', 'impar', 'par', 'impar', 'par', 'impar', 'par', 'impar', 'par']

Las comprensiones de listas son una forma elegante y eficiente de construir listas en Python. Son fáciles de leer y escribir, y pueden ayudarte a escribir código más limpio y conciso.

Ejercicios:

  • Ejercicio 1: Dada una lista de números, crea una lista que contenga los cuadrados de los números pares presentes en la lista dada.

  • Ejercicio 2: Dada una lista de cadenas, crea una nueva lista que contenga todas las cadenas convertidas a mayúsculas. Por ejemplo, si tienes la lista siguiente:

palabras = ['hola', 'python', 'mundo', 'programación']

Lo que quieres obtener es:

['HOLA', 'PYTHON', 'MUNDO', 'PROGRAMACIÓN']
  • Ejercicio 3: Dada una lista de números enteros, crea una lista que contenga solo los números primos de la lista dada, sin definir una función adicional.

4.7 Numpy

NumPy es una librería fundamental para la computación científica en Python. Proporciona soporte para arrays multidimensionales, junto con una amplia colección de funciones matemáticas para trabajar con estos arrays. Ayuda a usar de manera eficiente la memoria ya que contien operaciones numéricas rápidas.

Nota: Basado en Scientific Python Lectures y ASPP Latam.

Para instalar NumPy, pueden utilizar pip, el gestor de paquetes de Python:

pip install numpy

Primero, importemos NumPy en nuestro entorno de trabajo. Por convención, numpy se suele importar de la siguiente forma:

import numpy as np

Podemos crear arreglos unidimensionales o multidimensionales. Por ejemplo, para crear un array unidimensional:

arr_1d = np.array([1, 2, 3, 4, 5])
print(arr_1d)
## [1 2 3 4 5]

Tenemos una función para crear array de ceros:

np.zeros(10)
## array([0., 0., 0., 0., 0., 0., 0., 0., 0., 0.])

Y arrays de unos:

np.ones(10)
## array([1., 1., 1., 1., 1., 1., 1., 1., 1., 1.])

Otra forma de crear array es usando secuencias con arange, por ejemplo:

np.arange(10)
## array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])

O usando una notación similar a las rebanas: arange(inicio, fin, salto):

np.arange(9, -1, -1)
## array([9, 8, 7, 6, 5, 4, 3, 2, 1, 0])

También podemos crearlos usando valores aleatorios:

np.random.randint(0, 10, 5)
## array([9, 3, 0, 2, 0])

Para saber la dimensión, forma o longitud de nuestro array usamos lo siguiente:

a = np.array([0,1,2,3,4,5])
print(a)
## [0 1 2 3 4 5]
dimension = a.ndim
print(dimension)
## 1
forma = a.shape
print(forma)
## (6,)
longitud = len(a)
print(longitud)
## 6

Los arreglos multidimensionales, los podemos crear igual de varias maneras indicando las entradas de cada dimensión. Por ejemplo, para crear un array 2-dimensional de puros ceros, unos o random:

np.zeros((2,10))
## array([[0., 0., 0., 0., 0., 0., 0., 0., 0., 0.],
##        [0., 0., 0., 0., 0., 0., 0., 0., 0., 0.]])
np.ones((2,10))
## array([[1., 1., 1., 1., 1., 1., 1., 1., 1., 1.],
##        [1., 1., 1., 1., 1., 1., 1., 1., 1., 1.]])
np.random.randint(0, 10, (2, 10))
## array([[6, 6, 9, 5, 6, 7, 4, 3, 0, 9],
##        [2, 2, 5, 7, 5, 0, 5, 6, 4, 8]])

Otro array de dimensión dos puede ser el siguiente:

arr_2d = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
print(arr_2d)
## [[1 2 3]
##  [4 5 6]
##  [7 8 9]]
dimension = arr_2d.ndim
print(dimension)
## 2
forma = arr_2d.shape
print(forma)
## (3, 3)
longitud = len(arr_2d)
print(longitud)
## 3

De igual forma podemos usar arange:

np.arange(2 * 10).reshape(2,10)
## array([[ 0,  1,  2,  3,  4,  5,  6,  7,  8,  9],
##        [10, 11, 12, 13, 14, 15, 16, 17, 18, 19]])

Otra forma de crear arrays es por medio de la función linspace, esto nos crea una colección de puntos, a esta función se le debe indicar el inicio, fin y cantidad de punto, por ejemplo:

mi_array = np.linspace(0, 1, 6)
print(mi_array)
## [0.  0.2 0.4 0.6 0.8 1. ]
mi_array2 = np.linspace(0, 1, 5, endpoint = False)
print(mi_array2)
## [0.  0.2 0.4 0.6 0.8]

Otros dos arrays comunes son los diagonales:

diag_unos = np.eye(3)
print(diag_unos)
## [[1. 0. 0.]
##  [0. 1. 0.]
##  [0. 0. 1.]]

diagonal = np.diag([1,2,3,4]) 
#diagonal = np.diag(np.array([1,2,3,4])) 

4.7.1 Operaciones Básicas con NumPy

  1. Suma de Arrays
arr1 = np.array([[1, 2], [3, 4]])
arr2 = np.array([[5, 6], [7, 8]])


sum_arr = arr1 + arr2
print(sum_arr)
## [[ 6  8]
##  [10 12]]
  1. Producto de Arrays
prod_arr = arr1 * arr2
print(prod_arr)
## [[ 5 12]
##  [21 32]]
  1. Funciones Matemáticas: como calcular la raíz cuadrada
sqrt_arr = np.sqrt(arr1)
print(sqrt_arr)
## [[1.         1.41421356]
##  [1.73205081 2.        ]]
  1. Funciones Estadísticas:
arr = np.array([1, 2, 3, 4, 5])
print(np.mean(arr))   # Media
## 3.0
print(np.median(arr)) # Mediana
## 3.0
print(np.std(arr))    # Desviación estándar
## 1.4142135623730951

Ejercicios

Ejercicio 1: Crea un array unidimensional con los números del 1 al 10 e imprímelo.

Ejercicio 2: Crea una matriz 3x3 con todos los elementos iguales a 5 e imprímela.

Ejercicio 3: Calcula la suma de los elementos de la siguiente matriz:

matriz = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]])

4.7.2 Indexación y Rebanado

Como en las listas, los elementos de un array tiene un índice. Existen varias formas para indexar un array.

  1. Acceso a campos (field access indexing): Esta forma de indexación implica acceder a un elemento de un array multidimensional mediante una secuencia de índices separados por corchetes. Por ejemplo, a[1][2][3] significa que estás accediendo al elemento en la fila 1, columna 2 y profundidad 3 del array a.
# para arreglos unidimensionales
arr = np.array([1, 2, 3, 4, 5])
print(arr[2])   # Imprime el tercer elemento (índice 2)
## 3
print(arr[1:4]) # Imprime elementos desde el índice 1 hasta el 3
## [2 3 4]
# Crear un array tridimensional
a = np.array([[[1, 2, 3], [4, 5, 6]], [[7, 8, 9], [10, 11, 12]]])
# Acceder al elemento en la fila 1, columna 0, y profundidad 1
print(a[1][0][1])  # Imprime 8
## 8
  1. Indexación regular (regular indexing): En esta forma de indexación, los índices se proporcionan como una tupla separada por comas. Por ejemplo, a[1,2,3] significa que estás accediendo al elemento en la fila 1, columna 2 y profundidad 3 del array a.
# Crear un array tridimensional
a = np.array([[[1, 2, 3], [4, 5, 6]], [[7, 8, 9], [10, 11, 12]]])
# Acceder al elemento en la fila 1, columna 0, y profundidad 1
print(a[1, 0, 1])  # Imprime 8
## 8
  1. Rebanado (slicing): Esta técnica implica seleccionar una parte del array utilizando rangos de índices. Por ejemplo, a[1:3, 1:3] significa que estás seleccionando las filas de la 1 a la 2 (exclusivo) y las columnas de la 1 a la 2 (exclusivo) del array a.
# Crear un array bidimensional
a = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
# Seleccionar una parte del array
print(a[1:3, 1:3])  # Imprime [[5 6] [8 9]]
## [[5 6]
##  [8 9]]
  1. Indexación extravagante (fancy indexing): En esta forma de indexación, los índices se proporcionan como listas de índices. Por ejemplo, a[[1,2], [1,2]] significa que estás seleccionando los elementos con índices (1,1) y (2,2) del array a.
# Crear un array bidimensional
a = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
# Seleccionar elementos con índices específicos
print(a[[1, 2], [1, 2]])  # Imprime [5 9]
## [5 9]

De esta forma también podemos asignar nuevos valores a nuestro array:

a = np.arange(0, 100, 10)
# extraemos unos valores:
a[[2, 3, 2, 4, 2]]
## array([20, 30, 20, 40, 20])
# o modificamos algunos valores
a[[9, 7]] = -100
a
## array([   0,   10,   20,   30,   40,   50,   60, -100,   80, -100])
Imagen de Scientific Python Lectures, ver índice para la referencia.
Imagen de Scientific Python Lectures, ver índice para la referencia.

Otra forma de hacer este indexado fancy es usando boolean mask:

# creamos un generador de numeros aleatorios con semilla 27
rng = np.random.default_rng(27446968)
# creamos array con esos números aleatorios
a1 = rng.integers(0, 21, 15)
# Lo siguiente nos regresa un arreglo booleano
(a1 % 3 == 0)
## array([ True, False,  True, False, False, False,  True, False, False,
##        False,  True, False,  True, False,  True])
# Creamos una máscara con esa condicion
mascara = (a1 % 3 == 0)
# extraemos usando esa máscara
extraer_de_a1 = a1[mascara]
extraer_de_a1
## array([ 3, 12, 18,  6, 12,  3], dtype=int64)

Se puede emplear esto de las máscaras para asignar nuevos valores:

a1[a1 % 3 == 0] = -1
a1
## array([-1, 13, -1, 10, 10, 10, -1,  4,  8,  5, -1, 11, -1, 17, -1],
##       dtype=int64)

Ejercicio 1: Considera es siguiente array, extrae usando este tipo de indexado las letras O,M,G y W, O, Z.

np.array(list("ABCDEFGHIJKLMNOPQRSTUVWXYZ1234")).reshape(5,6)
## array([['A', 'B', 'C', 'D', 'E', 'F'],
##        ['G', 'H', 'I', 'J', 'K', 'L'],
##        ['M', 'N', 'O', 'P', 'Q', 'R'],
##        ['S', 'T', 'U', 'V', 'W', 'X'],
##        ['Y', 'Z', '1', '2', '3', '4']], dtype='<U1')

Ejercicio 2: Considerea el siguiente array y extrae el array [ 5, 6, 7, 8, 9, 10, 11] usando una máscara boleana:

np.arange(12).reshape(4,3)
## array([[ 0,  1,  2],
##        [ 3,  4,  5],
##        [ 6,  7,  8],
##        [ 9, 10, 11]])

Existe un argumento llamado where que funciona con las funciones universales como suma, media, etc.

a = np.arange(20).reshape(5,4)
cond = a >= 10
suma1 = np.sum(a[cond])
print(suma1)
## 145
suma2 = np.sum(a, where = cond)
print(suma2)
## 145

4.7.3 Vistas y copias

  1. Vista (View): Una vista es una manera de acceder a los datos de un array NumPy sin copiar el buffer de datos subyacente. Esto significa que una vista comparte el mismo buffer de datos que el array original, pero puede tener una forma o una distribución de datos diferentes.

Características:

  • Acceso al array sin cambiar el buffer de datos.

  • Indexación regular y rebanado generan vistas.

  • Las operaciones in-place se pueden realizar en vistas.

  1. Copia (Copy): Una copia es una creación de un nuevo array que duplica tanto los datos como la metadata del array original. Esto significa que una copia tiene su propio buffer de datos independiente del array original.

Características:

  • Se crea un nuevo array duplicando tanto los datos como la metadata del array original.

  • La indexación elegante siempre genera copias.

  • Se puede forzar una copia utilizando el método .copy().

a = np.arange(1, 6)
print(a)
## [1 2 3 4 5]
# Crear una vista del array y imprimir su base
v = a[2:5]  # v es una vista
print(v)
## [3 4 5]
print('v.base:', v.base)  # Devuelve el array 'base'
## v.base: [1 2 3 4 5]
a = np.arange(1, 6)
print(a)
## [1 2 3 4 5]
# Crear una copia del array
# En este caso, utilizando indexación elegante
v = a[[1, 2]]
print(v)
## [2 3]
# Cambiar un elemento en la copia y luego imprimir el array original
v[0] = 99

print('v:', v)
## v: [99  3]
print('a:', a)
## a: [1 2 3 4 5]

Como observamos, al crear una copia real no se modifica el array original. Vamos a forzar una copia también:

a = np.arange(1, 6)
w = a.copy()

w.base # vemos que indica None, lo que quiere decir es que no tiene como base 
# al array origianl, son idependientes
w.base == None
## True

Si usamos una vista:

a = np.arange(1, 6)
c = a[1:3]
c[0] = 8
print('c:', c)
## c: [8 3]
print('a:', a)
## a: [1 8 3 4 5]

Es importante entender la diferencia entre una vista y una copia cuando trabajas con arrays en NumPy. Las operaciones en vistas pueden modificar el array original, mientras que las operaciones en copias no afectarán al array original. Esto puede ser crucial, especialmente cuando se realizan operaciones in-place o se manipulan grandes conjuntos de datos.

4.7.4 Ordenamiento

La función de ordenamiento en NumPy se implementa con la función np.sort(). Por defecto, se utiliza el algoritmo de ordenamiento quicksort, que es muy eficiente.

Características:

  • np.sort() devuelve un array ordenado.

  • .sort() realiza el ordenamiento in-place en el array original.

  • Para ordenar en orden descendente, simplemente utiliza .flip() después de ordenar.

Para ordenamiento de un array unidimensional:

# Crear un array de números enteros aleatorios
a = np.random.randint(1, 10, 5)
print('Array original:', a)
## Array original: [9 2 6 9 9]
# Ordenar el array y mostrar el resultado
s = np.sort(a)
print('Array ordenado:', s)
## Array ordenado: [2 6 9 9 9]
# Ordenar in-place (en el mismo array)
a.sort()
print('Array original después de ordenar in-place:', a)
## Array original después de ordenar in-place: [2 6 9 9 9]

Para ordenamiento de un array bidimensional:

# Crear un array bidimensional de números enteros aleatorios
a = np.random.randint(1, 15, (3,3))
print('Array original:')
## Array original:
print(a)
## [[14 12  4]
##  [ 5  6 14]
##  [14 13  8]]
# Ordenar por filas
sorted_rows = np.sort(a)
print('Array ordenado por filas:')
## Array ordenado por filas:
print(sorted_rows)
## [[ 4 12 14]
##  [ 5  6 14]
##  [ 8 13 14]]
# Ordenar por columnas
sorted_cols = np.sort(a, axis=0)
print('Array ordenado por columnas:')
## Array ordenado por columnas:
print(sorted_cols)
## [[ 5  6  4]
##  [14 12  8]
##  [14 13 14]]

Otra función de ordenamiento es argsort:

# Crear un array de números enteros aleatorios
a = np.random.randint(1, 15, 10)
print('Array original:', a)
## Array original: [11 10  6 10  5  4 11 11  2  4]
# Obtener los índices que ordenarían el array
indices_ordenados = np.argsort(a)
print('Índices ordenados:', indices_ordenados)
## Índices ordenados: [8 5 9 4 2 1 3 0 6 7]
# Ordenar el array utilizando indexación elegante
sorted_array = a[indices_ordenados]
print('Array ordenado con indexación elegante:')
## Array ordenado con indexación elegante:
print(sorted_array)
## [ 2  4  4  5  6 10 10 11 11 11]

La función de ordenamiento en NumPy es una herramienta poderosa para ordenar arrays de manera eficiente, tanto en una como en varias dimensiones. Al entender cómo utilizar np.sort() y argsort(), puedes ordenar tus datos según tus necesidades específicas, ya sea en orden ascendente o descendente. Además, ten en cuenta que el argumento axis te permite ordenar por filas o columnas, según sea necesario.

Ejercicio 1: Para el array a=np.ones((5,5,5)), responde lo siguiente para cada caso:

  • ¿Cuál es su dimensión?

  • ¿Es una vista o una copia?

Después verifica tu respuesta.

  1. v = a[1, ::2, ::2]

  2. v = a[2,:]

  3. v = a[[0, 1],:]

  4. v = a[[2,3], [2,3]]

Ejercicio 2: Recrea los siguientes plots usando lo siguiente:

  • Realiza un array de solo ceros que será tu base.

  • Cambia los valores correspondientes a 1.

  • Usa plt.matshow(nombre_array) para hacer el plot.

Para importar el paquete para hacer el plot realiza lo siguiente:

%matplotlib inline
from matplotlib import pyplot as plt #libreria para hacer plots
Ejercicio de ASPP Latam
Ejercicio de ASPP Latam

Ejercicio 3: Comienza creando un array 3x3:

ej = np.array([[6,2,3], [1,7,2], [7,6,5]])
ej
## array([[6, 2, 3],
##        [1, 7, 2],
##        [7, 6, 5]])
  1. Ordena la última fila en forma ascenderte de manera in-place.

  2. Ahora, ordena la primera y segunda columna (ascendentemente) de dos formas distintas:

  • Usando fancy-index

  • in-place, usando rebanadas o índices.

4.7.5 Broadcasting

El broadcasting es una poderosa característica de NumPy que permite realizar operaciones aritméticas entre arrays de diferentes formas sin necesidad de que tengan las mismas dimensiones. Esto significa que NumPy puede manejar automáticamente situaciones donde las dimensiones de los arrays no coinciden, pero pueden ser compatibles de alguna manera.

Cuando se realizan operaciones aritméticas entre dos arrays, NumPy sigue un conjunto de reglas para determinar si las formas de los arrays son compatibles para la operación de broadcasting. Las reglas son las siguientes:

  1. Si los arrays tienen un número diferente de dimensiones, se les agrega una dimensión extra en el lado izquierdo hasta que ambos tengan el mismo número de dimensiones.
  2. Si las formas de los arrays no coinciden en alguna dimensión, NumPy ajustará las dimensiones de tamaño 1 para que coincidan en esa dimensión.
  3. Después de aplicar las reglas 1 y 2, los tamaños de las dimensiones de ambos arrays deben coincidir. Si alguna dimensión tiene un tamaño diferente y no es 1, se producirá un error.

Ejemplo 1: Suma de un escalar a un array

# Crear un array de forma (3, 3)
a = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]])

# Sumar un escalar (5) a cada elemento del array
resultado = a + 5
print(resultado)
## [[ 6  7  8]
##  [ 9 10 11]
##  [12 13 14]]

Ejemplo 2: Suma de dos arrays con diferentes formas

# Crear dos arrays con diferentes formas
a = np.array([[1, 2, 3], [4, 5, 6]])
b = np.array([10, 20, 30])

# Sumar ambos arrays
resultado = a + b
print(resultado)
## [[11 22 33]
##  [14 25 36]]

Ejemplo 3: Multiplicación de arrays con formas compatibles

# Crear dos arrays con formas compatibles
a = np.array([[1], [2], [3]])
b = np.array([1, 2, 3])

# Multiplicar ambos arrays
resultado = a * b
print(resultado)
## [[1 2 3]
##  [2 4 6]
##  [3 6 9]]

El broadcasting es una característica poderosa de NumPy que simplifica las operaciones entre arrays con diferentes formas. Al entender las reglas de broadcasting, puedes realizar operaciones aritméticas de manera eficiente y elegante en NumPy, lo que facilita el trabajo con datos multidimensionales en Python.

4.8 Visualización de datos

Matplotlib es la biblioteca principal de visualización de datos en Python. Proporciona una amplia gama de funciones y herramientas para crear diversos tipos de gráficos, como gráficos de líneas, de dispersión, de barras, de pastel, entre otros. Matplotlib también es altamente personalizable, lo que te permite ajustar casi todos los aspectos de tus gráficos.

matplotlib.pyplot es un módulo dentro de Matplotlib que proporciona una interfaz de estilo MATLAB para crear gráficos, proporciona una interfaz orientada a objetos. Simplifica la creación de gráficos al proporcionar una serie de funciones que permiten generar rápidamente gráficos simples.

Para instalarlo podemos realizar lo siguiente:

pip install matplotlib

Antes de comenzar, debemos importarlo. Vamos a usar los alias usuales para importarlo.

import matplotlib as mpl
import matplotlib.pyplot as plt
import numpy as np

Cuando se trabaja con Matplotlib, los datos se representan en lo que se llama “Figuras”. Una figura puede entenderse como una ventana o un lienzo en el cual se puede dibujar un gráfico. Cada figura puede contener uno o más “Ejes” (Axes), que es el área donde se puede especificar puntos en términos de coordenadas x-y.

Por ejemplo, si están trabajando en un gráfico de dispersión bidimensional, los ejes representarían el plano cartesiano donde pueden colocar sus puntos de datos. Si están haciendo un gráfico de barras, los ejes representarían los ejes x, y donde se dibujarían las barras.

Una de las formas más simples de crear una figura con ejes en Matplotlib es utilizando la función pyplot.subplots(). Esta función crea una nueva figura y devuelve una tupla que contiene la figura y un solo conjunto de ejes. Luego, se pueden utilizar los métodos asociados con los ejes para dibujar datos en la figura.

Por ejemplo, la función Axes.plot() se utiliza para dibujar una línea o un marcador en los ejes. Pueden pasar sus datos de x,y como argumentos a esta función y Matplotlib dibujará los puntos correspondientes en el gráfico.

Una figura está formada por las siguientes partes:

Imagen del tutorial de Matplotlib
Imagen del tutorial de Matplotlib

Matplotlib espera que los datos de entrada sean de tipo numpy.array o numpy.ma.masked_array, o bien objetos que puedan convertirse en numpy.array utilizando la función numpy.asarray(). Estos tipos de datos son adecuados porque Matplotlib está integrado con NumPy y puede trabajar eficientemente con arreglos NumPy para realizar operaciones de trazado.

Sin embargo, hay otros tipos de datos que son similares a los arreglos pero que Matplotlib puede no entender correctamente. Por ejemplo:

  1. Objetos de datos de pandas: Aunque pandas es una biblioteca popular para el análisis de datos en Python, los objetos de datos de pandas, como Series y DataFrame, no son directamente compatibles con Matplotlib. Aunque puedes trazar datos de pandas directamente, a veces puede haber problemas de compatibilidad o resultados inesperados.

  2. Objetos de matriz de NumPy (numpy.matrix): Aunque los objetos de matriz de NumPy son similares a los arreglos de NumPy, pueden tener un comportamiento ligeramente diferente en algunas operaciones, lo que puede causar problemas al trazar datos.

La convención común para evitar estos problemas es convertir estos objetos de datos “similares a arreglos” en arreglos NumPy utilizando numpy.array() antes de trazarlos. Esto garantiza una mayor compatibilidad y evita posibles problemas de trazado.

4.8.1 Bases de un plot

Los dos enfoques principales para utilizar Matplotlib son:

1. Interfaz explícita u orientada a objetos (OO)

En este enfoque, tú creas explícitamente las figuras (Figure) y los ejes (Axes), y luego llamas a los métodos asociados con ellos para personalizar y agregar elementos a tus gráficos. Este enfoque se conoce como el “estilo orientado a objetos (OO)”.

Por ejemplo, puedes crear una figura y ejes usando plt.subplots() y luego llamar a métodos como ax.plot() o ax.scatter() para trazar tus datos en los ejes. Este enfoque es más flexible y poderoso, especialmente para gráficos más complejos o cuando deseas realizar modificaciones detalladas en tus gráficos. Por ejemplo:

# Creamos una figura y ejes utilizando plt.subplots()
fig, ax = plt.subplots()

# Datos
x = np.linspace(-5, 5, 100)


# Utilizamos Axes.plot() para dibujar los datos en los ejes
ax.plot(x, x**2, label= "$x^2$")
ax.plot(x, -x**2+5, label= "$-x^2+5$")

# Configuramos ejes, título y legendas
ax.set_xlabel('Eje X')
ax.set_ylabel('Eje Y')
ax.set_title('Gráfico de funciones cuadráticas')
ax.legend() 

# Mostramos el gráfico
plt.show()

2. Interfaz implícita o estilo de pyplot

En este enfoque, confías en la interfaz pyplot para crear y gestionar implícitamente las figuras y los ejes en segundo plano. Utilizas las funciones de pyplot para realizar tareas como crear figuras, trazar datos y personalizar gráficos.

Por ejemplo, puedes llamar a funciones como plt.plot() o plt.scatter() directamente para trazar tus datos sin necesidad de crear explícitamente una figura y ejes. Este enfoque es más conveniente para gráficos simples y rápidos, pero puede ser menos flexible cuando necesitas realizar modificaciones detalladas en tus gráficos.

# Creamos nuestra base del plot
x = np.linspace(-5, 5, 40)

# Definimos las funciones para hacer el plot
fx, gx = x**2, -x**2+5

# Usamos la función plot de pyplot indicando eje X y eje y
plt.plot(x, fx, label= "$x^2$")
plt.plot(x, gx, label= "$-x^2 + 5$")
plt.xlabel('Eje X')
plt.ylabel('Eje Y')
plt.title('Gráfico de funciones cuadráticas')
plt.legend()

# Mostramos el plot
plt.show()

4.8.1.1 Colores y tipos de líneas

Tipos de líneas, imagen de la Cheatsheets de Matplotlib
Tipos de líneas, imagen de la Cheatsheets de Matplotlib
# Creamos nuestra base del plot
x = np.linspace(-5, 5, 40)

# Definimos las funciones para hacer el plot
fx, gx = x**2, -x**2+5

#Cambiamos tamaño de figura
plt.figure(figsize=(10,6), dpi=80)

# Configuramos color y tipo de línea
plt.plot(x, fx, color="red", linewidth=2.5, linestyle="-", label= "$x^2$")
plt.plot(x, gx, color="green", linewidth=2.5, linestyle="--", label= "$-x^2 + 5$")

# Añadimos títulos al eje X, Y y título del plot
plt.xlabel('Eje X')
plt.ylabel('Eje Y')
plt.title('Gráfico de funciones cuadráticas')
plt.legend()

# Mostramos el plot
plt.show()

4.8.1.2 Configurar los límites de los ejes

x = np.linspace(-5, 5, 40)

# Definimos las funciones para hacer el plot
fx, gx = x**2, -x**2+5

#Cambiamos tamaño de figura
plt.figure(figsize=(10,6), dpi=80)

# Configuramos color y tipo de línea
plt.plot(x, fx, color="red", linewidth=2.5, linestyle="-", label= "$x^2$")
plt.plot(x, gx, color="green", linewidth=2.5, linestyle="-", label= "$-x^2 + 5$")

# Configuramos límites de los ejes
plt.xlim(-6,6)
## (-6.0, 6.0)
plt.ylim(fx.min()*1.1, fx.max()*1.1)
## (0.018080210387902567, 27.500000000000004)
#plt.ylim(gx.min()*1.1, fx.max()*1.1)

# Añadimos títulos al eje X, Y y título del plot
plt.xlabel('Eje X')
plt.ylabel('Eje Y')
plt.title('Gráfico de funciones cuadráticas')
plt.legend()

# Mostramos el plot
plt.show()

4.8.1.3 Configuración de ticks

x = np.linspace(-5, 5, 40)

# Definimos las funciones para hacer el plot
fx, gx = x**2, -x**2+5

#Cambiamos tamaño de figura
plt.figure(figsize=(10,10), dpi=80)

# Configuramos color y tipo de línea
plt.plot(x, fx, color="red", linewidth=2.5, linestyle="-", label= "$x^2$")
plt.plot(x, gx, color="green", linewidth=2.5, linestyle="-", label= "$-x^2 + 5$")

# Configuramos límites de los ejes
plt.xlim(-6,6)
## (-6.0, 6.0)
plt.ylim(gx.min()*1.1, fx.max()*1.1)
## (-22.0, 27.500000000000004)
# Configuración de ticks
plt.xticks(np.arange(-5,6,1))
## ([<matplotlib.axis.XTick object at 0x0000012D3A1923D0>, <matplotlib.axis.XTick object at 0x0000012D3A3AFE90>, <matplotlib.axis.XTick object at 0x0000012D3A1DFDD0>, <matplotlib.axis.XTick object at 0x0000012D3A3F7050>, <matplotlib.axis.XTick object at 0x0000012D3A4010D0>, <matplotlib.axis.XTick object at 0x0000012D3A3F66D0>, <matplotlib.axis.XTick object at 0x0000012D3A403CD0>, <matplotlib.axis.XTick object at 0x0000012D3A405DD0>, <matplotlib.axis.XTick object at 0x0000012D3A407D50>, <matplotlib.axis.XTick object at 0x0000012D3A409D90>, <matplotlib.axis.XTick object at 0x0000012D3A40BBD0>], [Text(-5, 0, '−5'), Text(-4, 0, '−4'), Text(-3, 0, '−3'), Text(-2, 0, '−2'), Text(-1, 0, '−1'), Text(0, 0, '0'), Text(1, 0, '1'), Text(2, 0, '2'), Text(3, 0, '3'), Text(4, 0, '4'), Text(5, 0, '5')])
plt.yticks(np.arange(-20,30,5))
## ([<matplotlib.axis.YTick object at 0x0000012D3A1B74D0>, <matplotlib.axis.YTick object at 0x0000012D3A18B450>, <matplotlib.axis.YTick object at 0x0000012D39F6C2D0>, <matplotlib.axis.YTick object at 0x0000012D3A40DF90>, <matplotlib.axis.YTick object at 0x0000012D3A41C0D0>, <matplotlib.axis.YTick object at 0x0000012D3A41E110>, <matplotlib.axis.YTick object at 0x0000012D3A40B350>, <matplotlib.axis.YTick object at 0x0000012D3A424890>, <matplotlib.axis.YTick object at 0x0000012D3A425490>, <matplotlib.axis.YTick object at 0x0000012D3A428890>], [Text(0, -20, '−20'), Text(0, -15, '−15'), Text(0, -10, '−10'), Text(0, -5, '−5'), Text(0, 0, '0'), Text(0, 5, '5'), Text(0, 10, '10'), Text(0, 15, '15'), Text(0, 20, '20'), Text(0, 25, '25')])
# Añadimos títulos al eje X, Y y título del plot
plt.xlabel('Eje X')
plt.ylabel('Eje Y')
plt.title('Gráfico de funciones cuadráticas')
plt.legend()

# Mostramos el plot
plt.show()

4.8.1.4 Configurar label de los ticks

x = np.linspace(-5, 5, 40)

# Definimos las funciones para hacer el plot
fx, gx = x**2, -x**2+5

#Cambiamos tamaño de figura
plt.figure(figsize=(10,10), dpi=80)

# Configuramos color y tipo de línea
plt.plot(x, fx, color="red", linewidth=2.5, linestyle="-", label= "$x^2$")
plt.plot(x, gx, color="green", linewidth=2.5, linestyle="-", label= "$-x^2 + 5$")

# Configuramos límites de los ejes
plt.xlim(-6,6)
## (-6.0, 6.0)
plt.ylim(gx.min()*1.1, fx.max()*1.1)
## (-22.0, 27.500000000000004)
# Configuración de ticks
plt.xticks(np.arange(-2,3,0.5))
## ([<matplotlib.axis.XTick object at 0x0000012D34CB7990>, <matplotlib.axis.XTick object at 0x0000012D3A40CAD0>, <matplotlib.axis.XTick object at 0x0000012D3A3B7D90>, <matplotlib.axis.XTick object at 0x0000012D3A240850>, <matplotlib.axis.XTick object at 0x0000012D3A242B10>, <matplotlib.axis.XTick object at 0x0000012D3A243F90>, <matplotlib.axis.XTick object at 0x0000012D3A23DB10>, <matplotlib.axis.XTick object at 0x0000012D3A23FC10>, <matplotlib.axis.XTick object at 0x0000012D3A251B90>, <matplotlib.axis.XTick object at 0x0000012D3A253C10>], [Text(-2.0, 0, '−2.0'), Text(-1.5, 0, '−1.5'), Text(-1.0, 0, '−1.0'), Text(-0.5, 0, '−0.5'), Text(0.0, 0, '0.0'), Text(0.5, 0, '0.5'), Text(1.0, 0, '1.0'), Text(1.5, 0, '1.5'), Text(2.0, 0, '2.0'), Text(2.5, 0, '2.5')])
plt.yticks(np.arange(-20,30,5))
## ([<matplotlib.axis.YTick object at 0x0000012D3A406910>, <matplotlib.axis.YTick object at 0x0000012D3A48BA90>, <matplotlib.axis.YTick object at 0x0000012D3A45C750>, <matplotlib.axis.YTick object at 0x0000012D3A256B50>, <matplotlib.axis.YTick object at 0x0000012D3A260BD0>, <matplotlib.axis.YTick object at 0x0000012D3A262D10>, <matplotlib.axis.YTick object at 0x0000012D3A268C50>, <matplotlib.axis.YTick object at 0x0000012D3A2694D0>, <matplotlib.axis.YTick object at 0x0000012D3A26B3D0>, <matplotlib.axis.YTick object at 0x0000012D3A26D390>], [Text(0, -20, '−20'), Text(0, -15, '−15'), Text(0, -10, '−10'), Text(0, -5, '−5'), Text(0, 0, '0'), Text(0, 5, '5'), Text(0, 10, '10'), Text(0, 15, '15'), Text(0, 20, '20'), Text(0, 25, '25')])
# Añadimos títulos al eje X, Y y título del plot
plt.xlabel('Eje X')
plt.ylabel('Eje Y')
plt.title('Gráfico de funciones cuadráticas')
plt.legend()

# Mostramos el plot
plt.show()

x = np.linspace(-5, 5, 40)

# Definimos las funciones para hacer el plot
fx, gx = x**2, -x**2+5

#Cambiamos tamaño de figura
plt.figure(figsize=(10,10), dpi=80)

# Configuramos color y tipo de línea
plt.plot(x, fx, color="red", linewidth=2.5, linestyle="-", label= "$x^2$")
plt.plot(x, gx, color="green", linewidth=2.5, linestyle="-", label= "$-x^2 + 5$")

# Configuramos límites de los ejes
plt.xlim(-6,6)
## (-6.0, 6.0)
plt.ylim(gx.min()*1.1, fx.max()*1.1)
## (-22.0, 27.500000000000004)
# Configuración del label de los ticks
plt.xticks(np.arange(-2,3,0.5), [r'$-2$', r'$-\frac{3}{2}$', r'$-1$', r'$-\frac{1}{2}$', r'$0$',r'$\frac{1}{2}$', r'$1$', r'$\frac{3}{2}$', r'$2$', r'$\frac{5}{2}$'])
## ([<matplotlib.axis.XTick object at 0x0000012D3A45EAD0>, <matplotlib.axis.XTick object at 0x0000012D3A404410>, <matplotlib.axis.XTick object at 0x0000012D3A45C410>, <matplotlib.axis.XTick object at 0x0000012D3A2FE490>, <matplotlib.axis.XTick object at 0x0000012D3A308610>, <matplotlib.axis.XTick object at 0x0000012D3A2B1C50>, <matplotlib.axis.XTick object at 0x0000012D3A2A39D0>, <matplotlib.axis.XTick object at 0x0000012D3A28F750>, <matplotlib.axis.XTick object at 0x0000012D3A3F55D0>, <matplotlib.axis.XTick object at 0x0000012D3A46F3D0>], [Text(-2.0, 0, '$-2$'), Text(-1.5, 0, '$-\\frac{3}{2}$'), Text(-1.0, 0, '$-1$'), Text(-0.5, 0, '$-\\frac{1}{2}$'), Text(0.0, 0, '$0$'), Text(0.5, 0, '$\\frac{1}{2}$'), Text(1.0, 0, '$1$'), Text(1.5, 0, '$\\frac{3}{2}$'), Text(2.0, 0, '$2$'), Text(2.5, 0, '$\\frac{5}{2}$')])
plt.yticks(np.arange(-20,30,5))
## ([<matplotlib.axis.YTick object at 0x0000012D3A3A9AD0>, <matplotlib.axis.YTick object at 0x0000012D3A1C5A50>, <matplotlib.axis.YTick object at 0x0000012D3A2EC910>, <matplotlib.axis.YTick object at 0x0000012D3A23DA50>, <matplotlib.axis.YTick object at 0x0000012D3A2385D0>, <matplotlib.axis.YTick object at 0x0000012D3A23F3D0>, <matplotlib.axis.YTick object at 0x0000012D3A257190>, <matplotlib.axis.YTick object at 0x0000012D3A250890>, <matplotlib.axis.YTick object at 0x0000012D3A251A10>, <matplotlib.axis.YTick object at 0x0000012D3A240AD0>], [Text(0, -20, '−20'), Text(0, -15, '−15'), Text(0, -10, '−10'), Text(0, -5, '−5'), Text(0, 0, '0'), Text(0, 5, '5'), Text(0, 10, '10'), Text(0, 15, '15'), Text(0, 20, '20'), Text(0, 25, '25')])
# Añadimos títulos al eje X, Y y título del plot
plt.xlabel('Eje X')
plt.ylabel('Eje Y')
plt.title('Gráfico de funciones cuadráticas')
plt.legend()

# Mostramos el plot
plt.show()

4.8.1.5 Mover los “spines”

Los “spines” en Matplotlib son las líneas que conectan las marcas de los ejes y delimitan el área de los datos en un gráfico. Hay cuatro “spines” en total: superior, inferior, izquierdo y derecho. Tradicionalmente, los “spines” están ubicados en los bordes del área de los datos.

La idea de cambiar la posición de los “spines” es tenerlos en el centro del gráfico, en lugar de en los bordes. Esto puede ser útil para ciertos tipos de gráficos donde se desea resaltar la región central del gráfico.

  1. Descartar los “spines” superior y derecho: Para eliminar los “spines” superior y derecho, se establece su color en “none”, lo que hace que sean transparentes y no visibles en el gráfico.

  2. Mover los “spines” inferior y izquierdo al origen de coordenadas de los datos: Esto significa que los “spines” inferior y izquierdo se moverán para que se encuentren en la posición donde los valores de los ejes x e y son cero, respectivamente. Esto se hace para colocar los “spines” en el centro del gráfico.

x = np.linspace(-5, 5, 40)

# Definimos las funciones para hacer el plot
fx, gx = x**2, -x**2+5

#Cambiamos tamaño de figura
plt.figure(figsize=(10,10), dpi=80)

# Configuramos color y tipo de línea
plt.plot(x, fx, color="red", linewidth=2.5, linestyle="-", label= "$x^2$")
plt.plot(x, gx, color="green", linewidth=2.5, linestyle="-", label= "$-x^2 + 5$")

# Configuramos límites de los ejes
plt.xlim(-6,6)
## (-6.0, 6.0)
plt.ylim(gx.min()*1.1, fx.max()*1.1)
## (-22.0, 27.500000000000004)
# Configuración del label de los ticks
plt.xticks(np.arange(-2,3,0.5), [r'$-2$', r'$-\frac{3}{2}$', r'$-1$', r'$-\frac{1}{2}$', r'$0$',r'$\frac{1}{2}$', r'$1$', r'$\frac{3}{2}$', r'$2$', r'$\frac{5}{2}$'])
## ([<matplotlib.axis.XTick object at 0x0000012D3A424450>, <matplotlib.axis.XTick object at 0x0000012D3A2C4710>, <matplotlib.axis.XTick object at 0x0000012D3A26CD50>, <matplotlib.axis.XTick object at 0x0000012D3A3D4510>, <matplotlib.axis.XTick object at 0x0000012D3A3E8E10>, <matplotlib.axis.XTick object at 0x0000012D3A18FCD0>, <matplotlib.axis.XTick object at 0x0000012D3A18C5D0>, <matplotlib.axis.XTick object at 0x0000012D3A3A95D0>, <matplotlib.axis.XTick object at 0x0000012D3A3A9C10>, <matplotlib.axis.XTick object at 0x0000012D3A18A150>], [Text(-2.0, 0, '$-2$'), Text(-1.5, 0, '$-\\frac{3}{2}$'), Text(-1.0, 0, '$-1$'), Text(-0.5, 0, '$-\\frac{1}{2}$'), Text(0.0, 0, '$0$'), Text(0.5, 0, '$\\frac{1}{2}$'), Text(1.0, 0, '$1$'), Text(1.5, 0, '$\\frac{3}{2}$'), Text(2.0, 0, '$2$'), Text(2.5, 0, '$\\frac{5}{2}$')])
plt.yticks([-20,-10,10,20],[r'$-20$', r'$-10$', r'$+10$', r'$+20$'])
## ([<matplotlib.axis.YTick object at 0x0000012D3A2C4C90>, <matplotlib.axis.YTick object at 0x0000012D3A49E210>, <matplotlib.axis.YTick object at 0x0000012D3A18AB90>, <matplotlib.axis.YTick object at 0x0000012D3A1EF690>], [Text(0, -20, '$-20$'), Text(0, -10, '$-10$'), Text(0, 10, '$+10$'), Text(0, 20, '$+20$')])
# Mover los spines
ax = plt.gca() # obtener los ejes actuales
ax.spines['right'].set_color('none') 
ax.spines['top'].set_color('none')

ax.spines['bottom'].set_position('zero')
ax.spines['left'].set_position('zero')

# Otra forma de lograr lo anterior
#ax.xaxis.set_ticks_position('bottom')
#ax.spines['bottom'].set_position(('data',0))
#ax.yaxis.set_ticks_position('left')
#ax.spines['left'].set_position(('data',0))

# Añadimos títulos al eje X, Y y título del plot
plt.xlabel('Eje X')
plt.ylabel('Eje Y')
plt.title('Gráfico de funciones cuadráticas')
plt.legend()

# Mostramos el plot
plt.show()

4.8.1.6 Añadir y configurar legendas

Posición de legendas, imagen de las Cheatsheets de Matplotlib
Posición de legendas, imagen de las Cheatsheets de Matplotlib
x = np.linspace(-5, 5, 40)

# Definimos las funciones para hacer el plot
fx, gx = x**2, -x**2+5

#Cambiamos tamaño de figura
plt.figure(figsize=(10,10), dpi=80)

# Configuramos color y tipo de línea
plt.plot(x, fx, color="red", linewidth=2.5, linestyle="-", label= "$x^2$")
plt.plot(x, gx, color="green", linewidth=2.5, linestyle="-", label= "$-x^2 + 5$")

# Configuramos límites de los ejes
plt.xlim(-6,6)
## (-6.0, 6.0)
plt.ylim(gx.min()*1.1, fx.max()*1.1)
## (-22.0, 27.500000000000004)
# Configuración del label de los ticks
plt.xticks(np.arange(-2,3,0.5), [r'$-2$', r'$-\frac{3}{2}$', r'$-1$', r'$-\frac{1}{2}$', r'$0$',r'$\frac{1}{2}$', r'$1$', r'$\frac{3}{2}$', r'$2$', r'$\frac{5}{2}$'])
## ([<matplotlib.axis.XTick object at 0x0000012D3A216810>, <matplotlib.axis.XTick object at 0x0000012D3A2C5010>, <matplotlib.axis.XTick object at 0x0000012D3A2DB790>, <matplotlib.axis.XTick object at 0x0000012D3A2616D0>, <matplotlib.axis.XTick object at 0x0000012D3A46ECD0>, <matplotlib.axis.XTick object at 0x0000012D3A18B9D0>, <matplotlib.axis.XTick object at 0x0000012D3A2EE290>, <matplotlib.axis.XTick object at 0x0000012D3A1F5910>, <matplotlib.axis.XTick object at 0x0000012D3A3ABF90>, <matplotlib.axis.XTick object at 0x0000012D3A3D6A50>], [Text(-2.0, 0, '$-2$'), Text(-1.5, 0, '$-\\frac{3}{2}$'), Text(-1.0, 0, '$-1$'), Text(-0.5, 0, '$-\\frac{1}{2}$'), Text(0.0, 0, '$0$'), Text(0.5, 0, '$\\frac{1}{2}$'), Text(1.0, 0, '$1$'), Text(1.5, 0, '$\\frac{3}{2}$'), Text(2.0, 0, '$2$'), Text(2.5, 0, '$\\frac{5}{2}$')])
plt.yticks([-20,-10,10,20],[r'$-20$', r'$-10$', r'$+10$', r'$+20$'])
## ([<matplotlib.axis.YTick object at 0x0000012D3A261090>, <matplotlib.axis.YTick object at 0x0000012D3A144910>, <matplotlib.axis.YTick object at 0x0000012D3A18A5D0>, <matplotlib.axis.YTick object at 0x0000012D3A1C6C90>], [Text(0, -20, '$-20$'), Text(0, -10, '$-10$'), Text(0, 10, '$+10$'), Text(0, 20, '$+20$')])
# Mover los spines
ax = plt.gca() # obtener los ejes actuales
ax.spines['right'].set_color('none') 
ax.spines['top'].set_color('none')

ax.spines['bottom'].set_position('zero')
ax.spines['left'].set_position('zero')

# Otra forma de lograr lo anterior
#ax.xaxis.set_ticks_position('bottom')
#ax.spines['bottom'].set_position(('data',0))
#ax.yaxis.set_ticks_position('left')
#ax.spines['left'].set_position(('data',0))

# Añadimos títulos al eje X, Y y título del plot
plt.xlabel('Eje X')
plt.ylabel('Eje Y')
plt.title('Gráfico de funciones cuadráticas')

# Configurar posición de la leyenda
plt.legend(loc = 'center left')

#Otras configuraciones
#plt.legend(bbox_to_anchor=(1.05, 1), loc=2, borderaxespad=0.)
#plt.legend(bbox_to_anchor=(0., 1.02, 1., .102), loc=0, ncol=2, mode="expand", borderaxespad=0.)
#plt.legend(bbox_to_anchor=(1, 1), bbox_transform=plt.gcf().transFigure)


# Mostramos el plot
plt.show()

Se pueden poner por separado también:

x = np.linspace(-5, 5, 40)

# Definimos las funciones para hacer el plot
fx, gx = x**2, -x**2+5

#Cambiamos tamaño de figura
plt.figure(figsize=(10,10), dpi=80)

# Configuramos color y tipo de línea
line1, = plt.plot(x, fx, color="red", linewidth=2.5, linestyle="-", label= "$x^2$")
line2, = plt.plot(x, gx, color="green", linewidth=2.5, linestyle="-", label= "$-x^2 + 5$")

# Configuramos límites de los ejes
plt.xlim(-6,6)
## (-6.0, 6.0)
plt.ylim(gx.min()*1.1, fx.max()*1.1)
## (-22.0, 27.500000000000004)
# Configuración del label de los ticks
plt.xticks(np.arange(-2,3,0.5), [r'$-2$', r'$-\frac{3}{2}$', r'$-1$', r'$-\frac{1}{2}$', r'$0$',r'$\frac{1}{2}$', r'$1$', r'$\frac{3}{2}$', r'$2$', r'$\frac{5}{2}$'])
## ([<matplotlib.axis.XTick object at 0x0000012D34C2E1D0>, <matplotlib.axis.XTick object at 0x0000012D3A2FC710>, <matplotlib.axis.XTick object at 0x0000012D3A23F510>, <matplotlib.axis.XTick object at 0x0000012D3A1AE290>, <matplotlib.axis.XTick object at 0x0000012D3A1EF210>, <matplotlib.axis.XTick object at 0x0000012D3A18FE90>, <matplotlib.axis.XTick object at 0x0000012D39F84D10>, <matplotlib.axis.XTick object at 0x0000012D39FAE050>, <matplotlib.axis.XTick object at 0x0000012D3A41E3D0>, <matplotlib.axis.XTick object at 0x0000012D3A3B6450>], [Text(-2.0, 0, '$-2$'), Text(-1.5, 0, '$-\\frac{3}{2}$'), Text(-1.0, 0, '$-1$'), Text(-0.5, 0, '$-\\frac{1}{2}$'), Text(0.0, 0, '$0$'), Text(0.5, 0, '$\\frac{1}{2}$'), Text(1.0, 0, '$1$'), Text(1.5, 0, '$\\frac{3}{2}$'), Text(2.0, 0, '$2$'), Text(2.5, 0, '$\\frac{5}{2}$')])
plt.yticks([-20,-10,10,20],[r'$-20$', r'$-10$', r'$+10$', r'$+20$'])
## ([<matplotlib.axis.YTick object at 0x0000012D3A479250>, <matplotlib.axis.YTick object at 0x0000012D39F2AB90>, <matplotlib.axis.YTick object at 0x0000012D39F8A5D0>, <matplotlib.axis.YTick object at 0x0000012D3A1C7890>], [Text(0, -20, '$-20$'), Text(0, -10, '$-10$'), Text(0, 10, '$+10$'), Text(0, 20, '$+20$')])
# Mover los spines
ax = plt.gca() # obtener los ejes actuales
ax.spines['right'].set_color('none')
ax.spines['top'].set_color('none')
ax.spines['bottom'].set_position('zero')
ax.spines['left'].set_position('zero')

# Añadimos títulos al eje X, Y y título del plot
plt.xlabel('Eje X')
plt.ylabel('Eje Y')
plt.title('Gráfico de funciones cuadráticas')

# Leyenda para la primera función
first_legend = plt.legend(handles=[line1], loc=1)

# Añadir la leyenda de la primera función
ax = plt.gca().add_artist(first_legend)

# Crear la segunda leyenda
plt.legend(handles=[line2], loc=4)

# Mostramos el plot
plt.show()

Para ver ayuda sobre como usar las posiciones de las leyendas:

#help(plt.legend())

4.8.1.7 Realizar anotaciones

Tipos de flechas y estilos de anotaciones, cheatsheets de matplotlib
Tipos de flechas y estilos de anotaciones, cheatsheets de matplotlib
Tipos de flechas y estilos de anotaciones, cheatsheets de matplotlib
Tipos de flechas y estilos de anotaciones, cheatsheets de matplotlib
# Definimos las funciones para hacer el plot
x = np.linspace(-5, 5, 40)
fx, gx = x**2, -x**2+5

# Calculamos el punto máximo y mínimo de cada función
max_fx, min_fx = x[np.argmax(fx)], x[np.argmin(fx)]
max_gx, min_gx = x[np.argmax(gx)], x[np.argmin(gx)]

# Cambiamos tamaño de figura
plt.figure(figsize=(10,10), dpi=80)

# Configuramos color y tipo de línea
plt.plot(x, fx, color="red", linewidth=2.5, linestyle="-", label= "$x^2$")
plt.plot(x, gx, color="green", linewidth=2.5, linestyle="-", label= "$-x^2 + 5$")

# Configuramos límites de los ejes
plt.xlim(-6,6)
## (-6.0, 6.0)
plt.ylim(gx.min()*1.1, fx.max()*1.1)
## (-22.0, 27.500000000000004)
# Configuración del label de los ticks
plt.xticks(np.arange(-2,3,0.5), [r'$-2$', r'$-\frac{3}{2}$', r'$-1$', r'$-\frac{1}{2}$', r'$0$',r'$\frac{1}{2}$', r'$1$', r'$\frac{3}{2}$', r'$2$', r'$\frac{5}{2}$'])
## ([<matplotlib.axis.XTick object at 0x0000012D39F87950>, <matplotlib.axis.XTick object at 0x0000012D3A424410>, <matplotlib.axis.XTick object at 0x0000012D39F761D0>, <matplotlib.axis.XTick object at 0x0000012D3A1EE850>, <matplotlib.axis.XTick object at 0x0000012D3A28DA90>, <matplotlib.axis.XTick object at 0x0000012D3A28ED10>, <matplotlib.axis.XTick object at 0x0000012D3A3AEF50>, <matplotlib.axis.XTick object at 0x0000012D39F7F690>, <matplotlib.axis.XTick object at 0x0000012D3A3F4090>, <matplotlib.axis.XTick object at 0x0000012D3A43A9D0>], [Text(-2.0, 0, '$-2$'), Text(-1.5, 0, '$-\\frac{3}{2}$'), Text(-1.0, 0, '$-1$'), Text(-0.5, 0, '$-\\frac{1}{2}$'), Text(0.0, 0, '$0$'), Text(0.5, 0, '$\\frac{1}{2}$'), Text(1.0, 0, '$1$'), Text(1.5, 0, '$\\frac{3}{2}$'), Text(2.0, 0, '$2$'), Text(2.5, 0, '$\\frac{5}{2}$')])
plt.yticks([-20,-10,10,20],[r'$-20$', r'$-10$', r'$+10$', r'$+20$'])
## ([<matplotlib.axis.YTick object at 0x0000012D39FAD110>, <matplotlib.axis.YTick object at 0x0000012D39F825D0>, <matplotlib.axis.YTick object at 0x0000012D3A406690>, <matplotlib.axis.YTick object at 0x0000012D3A1ABC10>], [Text(0, -20, '$-20$'), Text(0, -10, '$-10$'), Text(0, 10, '$+10$'), Text(0, 20, '$+20$')])
# Mover los spines
ax = plt.gca() # obtener los ejes actuales
ax.spines['right'].set_color('none') 
ax.spines['top'].set_color('none')
ax.spines['bottom'].set_position('zero')
ax.spines['left'].set_position('zero')

# Añadimos títulos al eje X, Y y título del plot
plt.xlabel('Eje X')
plt.ylabel('Eje Y')
plt.title('Gráfico de funciones cuadráticas')

# Marcamos los puntos máximos y mínimos con anotaciones
plt.annotate('Min $(%.1f, %.1f)$' % (min_fx, min(fx)),
             xy=(min_fx, min(fx)), xycoords='data',
             xytext=(-120, -40), textcoords='offset points', fontsize=12,
             arrowprops=dict(arrowstyle="->", connectionstyle="arc3,rad=.2"))

plt.annotate('Máx $(%.1f, %.1f)$' % (max_gx, max(gx)),
             xy=(max_gx, max(gx)), xycoords='data',
             xytext=(+10, +30), textcoords='offset points', fontsize=12,
             arrowprops=dict(arrowstyle="->", connectionstyle="arc3,rad=.2"))


# Configurar posición de la leyenda
plt.legend(loc = 'center left')

# Mostramos el plot
plt.show()

Vamos a desglosar la función plt.annotate() y sus argumentos:

  1. 'Min $(%.1f, %.1f)$' % (min_fx, min(fx)): Esta parte crea el texto que se mostrará como la anotación. El % se utiliza para formatear la cadena con los valores de min_fx y min(fx), que son las coordenadas del punto mínimo en el eje x y y respectivamente. El .1f asegura que solo se muestre un decimal en el texto.

  2. xy=(min_fx, min(fx)): Establece las coordenadas del punto donde se colocará la anotación. En este caso, min_fx y min(fx) representan las coordenadas del punto mínimo en el gráfico.

  3. xycoords='data': Especifica el sistema de coordenadas utilizado para las coordenadas xy. En este caso, se refiere al sistema de coordenadas de los datos.

  4. xytext=(-50, -30): Especifica las coordenadas del texto de la anotación. En este caso, se desplaza el texto 50 puntos hacia la izquierda y 30 puntos hacia abajo desde la posición del punto.

  5. textcoords='offset points': Indica el sistema de coordenadas utilizado para las coordenadas xytext. En este caso, se utiliza un sistema de coordenadas desplazado desde el punto de datos.

  6. fontsize=12: Establece el tamaño de fuente del texto de la anotación.

  7. arrowprops=dict(arrowstyle="->", connectionstyle="arc3,rad=.2"): Define las propiedades de la flecha que se utilizará para señalar el punto de la anotación. En este caso, la flecha tendrá un estilo de punta de flecha (arrowstyle="->") y una conexión curva (connectionstyle="arc3,rad=.2"). La conexión curva se especifica con un radio de curvatura de 0.2 (rad=.2).

4.8.1.8 Más detalles

# Definimos las funciones para hacer el plot
x = np.linspace(-5, 5, 40)
fx, gx = x**2, -x**2+5

# Calculamos el punto máximo y mínimo de cada función
max_fx, min_fx = x[np.argmax(fx)], x[np.argmin(fx)]
max_gx, min_gx = x[np.argmax(gx)], x[np.argmin(gx)]

# Cambiamos tamaño de figura
plt.figure(figsize=(10,10), dpi=80)

# Configuramos color y tipo de línea
plt.plot(x, fx, color="red", linewidth=2.5, linestyle="-", label= "$x^2$")
plt.plot(x, gx, color="green", linewidth=2.5, linestyle="-", label= "$-x^2 + 5$")

# Configuramos límites de los ejes con cero centrado
plt.xlim(-5, 5)
## (-5.0, 5.0)
plt.ylim(-20, 20)
## (-20.0, 20.0)
# Configuración del label de los ticks
plt.xticks(np.arange(-2,3,0.5), [r'$-2$', r'$-\frac{3}{2}$', r'$-1$', r'$-\frac{1}{2}$', r'$0$',r'$\frac{1}{2}$', r'$1$', r'$\frac{3}{2}$', r'$2$', r'$\frac{5}{2}$'])
## ([<matplotlib.axis.XTick object at 0x0000012D39F74050>, <matplotlib.axis.XTick object at 0x0000012D3A190210>, <matplotlib.axis.XTick object at 0x0000012D3A497510>, <matplotlib.axis.XTick object at 0x0000012D368BDE50>, <matplotlib.axis.XTick object at 0x0000012D3A1C6950>, <matplotlib.axis.XTick object at 0x0000012D3A23E4D0>, <matplotlib.axis.XTick object at 0x0000012D39FA3DD0>, <matplotlib.axis.XTick object at 0x0000012D39FA20D0>, <matplotlib.axis.XTick object at 0x0000012D3A2C41D0>, <matplotlib.axis.XTick object at 0x0000012D3A46E310>], [Text(-2.0, 0, '$-2$'), Text(-1.5, 0, '$-\\frac{3}{2}$'), Text(-1.0, 0, '$-1$'), Text(-0.5, 0, '$-\\frac{1}{2}$'), Text(0.0, 0, '$0$'), Text(0.5, 0, '$\\frac{1}{2}$'), Text(1.0, 0, '$1$'), Text(1.5, 0, '$\\frac{3}{2}$'), Text(2.0, 0, '$2$'), Text(2.5, 0, '$\\frac{5}{2}$')])
plt.yticks([-20,-10,10,20],[r'$-20$', r'$-10$', r'$+10$', r'$+20$'])
## ([<matplotlib.axis.YTick object at 0x0000012D3A163DD0>, <matplotlib.axis.YTick object at 0x0000012D3A26C310>, <matplotlib.axis.YTick object at 0x0000012D3A3F42D0>, <matplotlib.axis.YTick object at 0x0000012D3A4007D0>], [Text(0, -20, '$-20$'), Text(0, -10, '$-10$'), Text(0, 10, '$+10$'), Text(0, 20, '$+20$')])
# Mover los spines
ax = plt.gca() # obtener los ejes actuales
ax.spines['right'].set_color('none') 
ax.spines['top'].set_color('none')
ax.spines['bottom'].set_position('zero')
ax.spines['left'].set_position('zero')

# Añadimos títulos al eje X, Y y título del plot
plt.xlabel('Eje X')
plt.ylabel('Eje Y')
plt.title('Gráfico de funciones cuadráticas')

# Marcamos los puntos máximos y mínimos con anotaciones
plt.annotate('Min $(%.1f, %.1f)$' % (min_fx, min(fx)),
             xy=(min_fx, min(fx)), xycoords='data',
             xytext=(-120, -40), textcoords='offset points', fontsize=12,
             arrowprops=dict(arrowstyle="->", connectionstyle="arc3,rad=.2"))

plt.annotate('Máx $(%.1f, %.1f)$' % (max_gx, max(gx)),
             xy=(max_gx, max(gx)), xycoords='data',
             xytext=(+10, +30), textcoords='offset points', fontsize=12,
             arrowprops=dict(arrowstyle="->", connectionstyle="arc3,rad=.2"))

for label in ax.get_xticklabels() + ax.get_yticklabels():
    label.set_fontsize(14)
    label.set_bbox(dict(facecolor='white', edgecolor='None', alpha=0.65))

# Configurar posición de la leyenda
plt.legend(loc = 'center left')

# Mostramos el plot
plt.show()

4.8.1.9 Subplots

Como vimos, una Figura en matplotlib es toda la ventana, toda la interface. Podemos crear varios subplots en una figura y configurar cada uno de ellos.

fig = plt.figure()  # Una figura vacía
fig, ax = plt.subplots()  # una figura con solo un axes
fig, axs = plt.subplots(2, 2)  # un grid de figuras 2x2
# una figura con un subplot a la izquierda y dos a la derecha
fig, axs = plt.subplot_mosaic([['left', 'right_top'],
                               ['left', 'right_bottom']])

Con el ejemplo de las funciones que hemos estado trabajando, podríamos separarlas en dos subplots como sigue.

# Definimos las funciones para hacer el plot
x = np.linspace(-5, 5, 40)
fx, gx = x**2, -x**2 + 5

# Calculamos el punto máximo y mínimo de cada función
max_fx, min_fx = x[np.argmax(fx)], x[np.argmin(fx)]
max_gx, min_gx = x[np.argmax(gx)], x[np.argmin(gx)]

# Configuramos tamaño de figura y creamos subplots
plt.figure(figsize=(10, 10))

# Subplot para la función fx = x^2
plt.subplot(2, 1, 1)
plt.plot(x, fx, color="red", linewidth=2.5, linestyle="-", label="$x^2$")
plt.xlim(-6, 6)
## (-6.0, 6.0)
plt.ylim(fx.min() * 1.1, fx.max() * 1.1)
## (0.018080210387902567, 27.500000000000004)
plt.xticks(np.arange(-2, 3, 0.5), [r'$-2$', r'$-\frac{3}{2}$', r'$-1$', r'$-\frac{1}{2}$', r'$0$', r'$\frac{1}{2}$', r'$1$', r'$\frac{3}{2}$', r'$2$', r'$\frac{5}{2}$'])
## ([<matplotlib.axis.XTick object at 0x0000012D368816D0>, <matplotlib.axis.XTick object at 0x0000012D368B0210>, <matplotlib.axis.XTick object at 0x0000012D3970E210>, <matplotlib.axis.XTick object at 0x0000012D36895590>, <matplotlib.axis.XTick object at 0x0000012D3A457250>, <matplotlib.axis.XTick object at 0x0000012D3A3BD250>, <matplotlib.axis.XTick object at 0x0000012D3A3BF090>, <matplotlib.axis.XTick object at 0x0000012D3A3E5090>, <matplotlib.axis.XTick object at 0x0000012D3A13AA10>, <matplotlib.axis.XTick object at 0x0000012D3A3E7050>], [Text(-2.0, 0, '$-2$'), Text(-1.5, 0, '$-\\frac{3}{2}$'), Text(-1.0, 0, '$-1$'), Text(-0.5, 0, '$-\\frac{1}{2}$'), Text(0.0, 0, '$0$'), Text(0.5, 0, '$\\frac{1}{2}$'), Text(1.0, 0, '$1$'), Text(1.5, 0, '$\\frac{3}{2}$'), Text(2.0, 0, '$2$'), Text(2.5, 0, '$\\frac{5}{2}$')])
plt.yticks([-20, -10, 10, 20], [r'$-20$', r'$-10$', r'$+10$', r'$+20$'])
## ([<matplotlib.axis.YTick object at 0x0000012D3686AA10>, <matplotlib.axis.YTick object at 0x0000012D3A26C310>, <matplotlib.axis.YTick object at 0x0000012D3A1CC2D0>, <matplotlib.axis.YTick object at 0x0000012D3A3CB650>], [Text(0, -20, '$-20$'), Text(0, -10, '$-10$'), Text(0, 10, '$+10$'), Text(0, 20, '$+20$')])
plt.xlabel('Eje X')
plt.ylabel('Eje Y')
plt.title('Función $x^2$')
plt.annotate('Min $(%.1f, %.1f)$' % (min_fx, min(fx)),
             xy=(min_fx, min(fx)), xycoords='data',
             xytext=(-120, -40), textcoords='offset points', fontsize=12,
             arrowprops=dict(arrowstyle="->", connectionstyle="arc3,rad=.2"))

plt.legend(loc='upper left')

# Subplot para la función gx = -x^2 + 5
plt.subplot(2, 1, 2)
plt.plot(x, gx, color="green", linewidth=2.5, linestyle="-", label="$-x^2 + 5$")
plt.xlim(-6, 6)
## (-6.0, 6.0)
plt.ylim(gx.min() * 1.1, gx.max() * 1.1)
## (-22.0, 5.481919789612098)
plt.xticks(np.arange(-2, 3, 0.5), [r'$-2$', r'$-\frac{3}{2}$', r'$-1$', r'$-\frac{1}{2}$', r'$0$', r'$\frac{1}{2}$', r'$1$', r'$\frac{3}{2}$', r'$2$', r'$\frac{5}{2}$'])
## ([<matplotlib.axis.XTick object at 0x0000012D34925710>, <matplotlib.axis.XTick object at 0x0000012D3A454050>, <matplotlib.axis.XTick object at 0x0000012D39F7DE10>, <matplotlib.axis.XTick object at 0x0000012D3A2AF390>, <matplotlib.axis.XTick object at 0x0000012D3A291590>, <matplotlib.axis.XTick object at 0x0000012D3A293590>, <matplotlib.axis.XTick object at 0x0000012D3A2956D0>, <matplotlib.axis.XTick object at 0x0000012D3A290A10>, <matplotlib.axis.XTick object at 0x0000012D3A297FD0>, <matplotlib.axis.XTick object at 0x0000012D39611E50>], [Text(-2.0, 0, '$-2$'), Text(-1.5, 0, '$-\\frac{3}{2}$'), Text(-1.0, 0, '$-1$'), Text(-0.5, 0, '$-\\frac{1}{2}$'), Text(0.0, 0, '$0$'), Text(0.5, 0, '$\\frac{1}{2}$'), Text(1.0, 0, '$1$'), Text(1.5, 0, '$\\frac{3}{2}$'), Text(2.0, 0, '$2$'), Text(2.5, 0, '$\\frac{5}{2}$')])
plt.yticks([-20, -10, 10, 20], [r'$-20$', r'$-10$', r'$+10$', r'$+20$'])
## ([<matplotlib.axis.YTick object at 0x0000012D34D0E850>, <matplotlib.axis.YTick object at 0x0000012D36894E10>, <matplotlib.axis.YTick object at 0x0000012D3A49C650>, <matplotlib.axis.YTick object at 0x0000012D3964A950>], [Text(0, -20, '$-20$'), Text(0, -10, '$-10$'), Text(0, 10, '$+10$'), Text(0, 20, '$+20$')])
plt.xlabel('Eje X')
plt.ylabel('Eje Y')
plt.title('Función $-x^2 + 5$')

plt.annotate('Máx $(%.1f, %.1f)$' % (max_gx, max(gx)),
             xy=(max_gx, max(gx)), xycoords='data',
             xytext=(+10, +30), textcoords='offset points', fontsize=12,
             arrowprops=dict(arrowstyle="->", connectionstyle="arc3,rad=.2"))
plt.legend(loc='upper left')

# Mostramos el plot
plt.tight_layout()
plt.show()

Con el método OO:

# Definimos las funciones para hacer el plot
x = np.linspace(-5, 5, 40)
fx, gx = x**2, -x**2 + 5

# Calculamos el punto máximo y mínimo de cada función
max_fx, min_fx = x[np.argmax(fx)], x[np.argmin(fx)]
max_gx, min_gx = x[np.argmax(gx)], x[np.argmin(gx)]

# Cambiamos tamaño de figura
fig, axs = plt.subplots(2, 1, figsize=(10, 10))

# Configuramos color y tipo de línea para la primera función
axs[0].plot(x, fx, color="red", linewidth=2.5, linestyle="-", label="$x^2$")
axs[0].set_xlim(-6, 6)
## (-6.0, 6.0)
axs[0].set_ylim(fx.min() * 1.1, fx.max() * 1.1)
## (0.018080210387902567, 27.500000000000004)
axs[0].set_xticks(np.arange(-2, 3, 0.5))
axs[0].set_yticks([-20, -10, 10, 20])
axs[0].set_xlabel('Eje X')
axs[0].set_ylabel('Eje Y')
axs[0].set_title('Función $x^2$')

# Anotaciones en el subplot de la primera función
axs[0].annotate('Min $(%.1f, %.1f)$' % (min_fx, min(fx)),
                xy=(min_fx, min(fx)), xycoords='data',
                xytext=(-120, -40), textcoords='offset points', fontsize=12,
                arrowprops=dict(arrowstyle="->", connectionstyle="arc3,rad=.2"))


# Configurar posición de la leyenda en el subplot de la primera función
axs[0].legend(loc='upper left')

# Configuramos color y tipo de línea para la segunda función
axs[1].plot(x, gx, color="green", linewidth=2.5, linestyle="-", label="$-x^2 + 5$")
axs[1].set_xlim(-6, 6)
## (-6.0, 6.0)
axs[1].set_ylim(gx.min() * 1.1, gx.max() * 1.1)
## (-22.0, 5.481919789612098)
axs[1].set_xticks(np.arange(-2, 3, 0.5))
axs[1].set_yticks([-20, -10, 10, 20])
axs[1].set_xticklabels([r'$-2$', r'$-\frac{3}{2}$', r'$-1$', r'$-\frac{1}{2}$', r'$0$', r'$\frac{1}{2}$', r'$1$', r'$\frac{3}{2}$', r'$2$', r'$\frac{5}{2}$'])
axs[1].set_yticklabels([r'$-20$', r'$-10$', r'$+10$', r'$+20$'])
axs[1].set_xlabel('Eje X')
axs[1].set_ylabel('Eje Y')
axs[1].set_title('Función $-x^2 + 5$')

# Anotaciones en el subplot de la segunda función

axs[1].annotate('Máx $(%.1f, %.1f)$' % (max_gx, max(gx)),
                xy=(max_gx, max(gx)), xycoords='data',
                xytext=(+10, +30), textcoords='offset points', fontsize=12,
                arrowprops=dict(arrowstyle="->", connectionstyle="arc3,rad=.2"))

# Configurar posición de la leyenda en el subplot de la segunda función
axs[1].legend(loc='upper left')

# Mostramos el plot
plt.tight_layout()
plt.show()

4.8.1.10 Paletas de colores

Existen varias paletas de colores que podemos usar.

from matplotlib import colormaps
list(colormaps)
## ['magma', 'inferno', 'plasma', 'viridis', 'cividis', 'twilight', 'twilight_shifted', 'turbo', 'Blues', 'BrBG', 'BuGn', 'BuPu', 'CMRmap', 'GnBu', 'Greens', 'Greys', 'OrRd', 'Oranges', 'PRGn', 'PiYG', 'PuBu', 'PuBuGn', 'PuOr', 'PuRd', 'Purples', 'RdBu', 'RdGy', 'RdPu', 'RdYlBu', 'RdYlGn', 'Reds', 'Spectral', 'Wistia', 'YlGn', 'YlGnBu', 'YlOrBr', 'YlOrRd', 'afmhot', 'autumn', 'binary', 'bone', 'brg', 'bwr', 'cool', 'coolwarm', 'copper', 'cubehelix', 'flag', 'gist_earth', 'gist_gray', 'gist_heat', 'gist_ncar', 'gist_rainbow', 'gist_stern', 'gist_yarg', 'gnuplot', 'gnuplot2', 'gray', 'hot', 'hsv', 'jet', 'nipy_spectral', 'ocean', 'pink', 'prism', 'rainbow', 'seismic', 'spring', 'summer', 'terrain', 'winter', 'Accent', 'Dark2', 'Paired', 'Pastel1', 'Pastel2', 'Set1', 'Set2', 'Set3', 'tab10', 'tab20', 'tab20b', 'tab20c', 'magma_r', 'inferno_r', 'plasma_r', 'viridis_r', 'cividis_r', 'twilight_r', 'twilight_shifted_r', 'turbo_r', 'Blues_r', 'BrBG_r', 'BuGn_r', 'BuPu_r', 'CMRmap_r', 'GnBu_r', 'Greens_r', 'Greys_r', 'OrRd_r', 'Oranges_r', 'PRGn_r', 'PiYG_r', 'PuBu_r', 'PuBuGn_r', 'PuOr_r', 'PuRd_r', 'Purples_r', 'RdBu_r', 'RdGy_r', 'RdPu_r', 'RdYlBu_r', 'RdYlGn_r', 'Reds_r', 'Spectral_r', 'Wistia_r', 'YlGn_r', 'YlGnBu_r', 'YlOrBr_r', 'YlOrRd_r', 'afmhot_r', 'autumn_r', 'binary_r', 'bone_r', 'brg_r', 'bwr_r', 'cool_r', 'coolwarm_r', 'copper_r', 'cubehelix_r', 'flag_r', 'gist_earth_r', 'gist_gray_r', 'gist_heat_r', 'gist_ncar_r', 'gist_rainbow_r', 'gist_stern_r', 'gist_yarg_r', 'gnuplot_r', 'gnuplot2_r', 'gray_r', 'hot_r', 'hsv_r', 'jet_r', 'nipy_spectral_r', 'ocean_r', 'pink_r', 'prism_r', 'rainbow_r', 'seismic_r', 'spring_r', 'summer_r', 'terrain_r', 'winter_r', 'Accent_r', 'Dark2_r', 'Paired_r', 'Pastel1_r', 'Pastel2_r', 'Set1_r', 'Set2_r', 'Set3_r', 'tab10_r', 'tab20_r', 'tab20b_r', 'tab20c_r']

Los colores los podemos indicar en diferentes formatos:

  • RGB o RGBA: (0.1, 0.2, 0.5), (0.1, 0.2, 0.5, 0.3)

  • Cadena de RGB o RGBA: '#0f0f0f', '#0f0f0f80'

  • Hex RGB o RGBA: '#abc' = 'aabbcc', '#fb1' = '#ffbb11'

  • Escala de grises en escala de [0,1]: '0' (negro), '1' (blanco), '0.8' (gris claro)

  • Abreviación de colores básicos:

    • ‘b’ = blue
    • ‘g’ = green
    • ‘r’ = red
    • ‘c’ = cyan
    • ‘m’ = magenta
    • ‘y’ = yellow
    • ‘k’ = black
    • ‘w’ = white
  • Nombre del color: 'aquamarine', 'mediumseagreen'

  • Del ciclo de colores default: C0, C1, donde el ciclo es ['#1f77b4', '#ff7f0e', '#2ca02c', '#d62728', '#9467bd', '#8c564b', '#e377c2', '#7f7f7f', '#bcbd22', '#17becf']

  • Tupla de color y valor de transparencia alfa: ('green',0.3)

A los colores les podemos indicar también un nivel de transparencia que se llama alpha:

## (-0.2, 13.0)
## (-1.0, 1.0)
## (-0.2, 13.0, -1.0, 1.0)

4.8.1.11 Orden de los elementos

El orden de dibujo de los artistas en Matplotlib está determinado por su atributo zorder, que es un número decimal flotante. Los artistas con un zorder más alto se dibujan encima de los artistas con un zorder más bajo. Esto es útil cuando se tienen múltiples elementos en un gráfico y se desea controlar qué elementos están encima de otros.

Por defecto, Matplotlib asigna un valor de zorder dependiendo del tipo de artista. Los artistas más comunes, como las líneas, los puntos y los contornos, suelen tener un valor de zorder predeterminado más alto para que se dibujen encima de otros elementos.

Por ejemplo, si se tiene una línea y un marcador en un gráfico, la línea se dibujará encima del marcador si tienen el mismo color y estilo. Sin embargo, si se desea que el marcador se dibuje encima de la línea, se puede ajustar el zorder del marcador para que sea mayor que el de la línea.

Z-order, del manual de matplotlib
Z-order, del manual de matplotlib
r = np.linspace(0.3, 1, 30)
theta = np.linspace(0, 4*np.pi, 30)
x = r * np.sin(theta)
y = r * np.cos(theta)

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

plt.subplot(1, 2, 1)
plt.plot(x, y, 'C3', lw=3)
plt.scatter(x, y, s=120)
plt.title('Líneas sobre los puntos')

plt.subplot(1, 2, 2)
plt.plot(x, y, 'C3', lw=3)
plt.scatter(x, y, s=120, zorder=2.5)  # mover los puntos encima de la línea
plt.title('Puntos sobre las líneas')

plt.tight_layout()
plt.show()

4.8.2 Tipos de plots

4.8.2.1 Áreas entre curvas

La función fill_between() en Matplotlib se utiliza para crear áreas sombreadas entre dos curvas o entre una curva y un eje. Esto puede ser útil para resaltar ciertas regiones en un gráfico.

# Definimos los datos
x = np.linspace(0, 10, 100)
y1 = np.sin(x)
y2 = np.cos(x)

# Creamos el gráfico
plt.figure(figsize=(8, 6))

# Graficamos las curvas
plt.plot(x, y1, color="#a1dab4", label='sin(x)')
plt.plot(x, y2, color="#253494", label='cos(x)')

# Rellenamos el área entre las curvas y=sen(x) y y=cos(x)
plt.fill_between(x, y1, y2, where=(y1 >= y2), color=(0.1, 0.2, 0.5), alpha=0.3, interpolate=True, label='Área entre curvas')

# Añadimos etiquetas y título
plt.xlabel('x')
plt.ylabel('y')
plt.title('Área sombreada entre curvas')

# Mover la leyenda fuera del gráfico
plt.legend(loc='upper left', bbox_to_anchor=(1, 1))

# Mostramos el gráfico
plt.grid(True)
plt.show()

El parámetro interpolate en la función fill_between() de Matplotlib controla si se debe interpolar entre los puntos de datos al rellenar el área entre las curvas.

Cuando interpolate está establecido en True, Matplotlib interpolará suavemente entre los puntos de datos para crear un área sombreada más suave entre las curvas. Esto puede ser útil cuando los datos están espaciados de manera irregular o cuando se desea una representación visualmente más atractiva del área sombreada.

4.8.2.2 Scatter plot o gráfico de dispersión

# Definimos los datos
np.random.seed(0)
n = 100
x = np.random.rand(n)
y = np.random.rand(n)
colors = np.arctan2(y, x)  # Calculamos el ángulo para determinar el color
sizes = 1000 * np.random.rand(n)  # Tamaño de los marcadores

# Creamos el gráfico
plt.figure(figsize=(8, 6))

# Graficamos los marcadores
plt.scatter(x, y, c=colors, s=sizes, alpha=0.5, cmap='hsv')

# Añadimos etiquetas y título
plt.xlabel('X')
plt.ylabel('Y')
plt.title('Gráfico de marcadores con color según el ángulo')

# Mostramos la barra de color
plt.colorbar(label='Ángulo')
## <matplotlib.colorbar.Colorbar object at 0x0000012D3AB18510>
# Mostramos el gráfico
plt.grid(True)
plt.show()

La función sns.scatterplot() es una herramienta de Seaborn para crear gráficos de dispersión. Este tipo de gráfico se utiliza comúnmente para visualizar la relación entre dos variables continuas.

Los argumentos principales de sns.scatterplot():

  1. x: Este es el primer argumento y representa los datos para el eje x del gráfico de dispersión. Puede ser una serie de pandas, un array de NumPy, una lista o cualquier otra estructura de datos similar que contenga los valores de la variable para el eje x.

  2. y: Este es el segundo argumento y representa los datos para el eje y del gráfico de dispersión. Al igual que el argumento x, puede ser una serie de pandas, un array de NumPy, una lista u otra estructura de datos similar que contenga los valores de la variable para el eje y.

  3. hue: Este argumento es opcional y se utiliza para asignar un color diferente a cada categoría de una variable categórica. Puede ser una serie de pandas, un array de NumPy, una lista u otra estructura de datos similar que contenga los valores de la variable categórica.

  4. size: Otro argumento opcional que se utiliza para controlar el tamaño de los puntos en el gráfico de dispersión. Puede ser una serie de pandas, un array de NumPy, una lista u otra estructura de datos similar que contenga los valores del tamaño de los puntos.

  5. sizes: Este argumento opcional permite especificar el rango de tamaños de los puntos en el gráfico de dispersión. Puede ser una tupla que contenga el tamaño mínimo y máximo que se asignará a los puntos en el gráfico.

  6. alpha: Este argumento opcional controla la transparencia de los puntos en el gráfico de dispersión. Un valor de 0 significa completamente transparente, mientras que un valor de 1 significa completamente opaco.

  7. palette: Este argumento opcional se utiliza para especificar la paleta de colores que se utilizará para mapear los valores de la variable categórica en el argumento hue a colores en el gráfico de dispersión.

La función tiene más opciones para personalizar aún más el aspecto del gráfico, como style, markers, edgecolor, linewidth, entre otros.

Ejercicio 1: Usando seaborn, carga la base de datos iris y crea un gráfico de dispersión con matplotlib (sin dividir por color la especie) de la longitud del sepalo vs la anchura del sepalo.

import seaborn as sns

# Cargar el conjunto de datos de iris
iris = sns.load_dataset('iris')

Si usamos la siguiente opción de seaborn, podríamos crear un gráfico de dispersión de la longitud del pétalo vs su anchura de la base de datos iris donde estén coloreados por especie.

# Cargar la base de datos Iris
iris = sns.load_dataset('iris')

# Crear el gráfico de dispersión con Seaborn
plt.figure(figsize=(8, 6))
sns.scatterplot(data=iris, x='petal_length', y='petal_width', hue='species')
plt.xlabel('Longitud del Pétalo (cm)')
plt.ylabel('Anchura del Pétalo (cm)')
plt.title('Gráfico de dispersión de longitud vs anchura del pétalo por especie')
plt.grid(True)
plt.show()

Con matplotlib una estrategía para hacer un plot similar es la siguiente:

# Cargar la base de datos Iris


# Creamos el scatter plot
plt.figure(figsize=(8, 6))

# Scatter plot para la especie 'setosa'
setosa = plt.scatter(iris[iris['species'] == 'setosa']['sepal_length'], iris[iris['species'] == 'setosa']['sepal_width'], c='C0', cmap='viridis', s=50, alpha=0.7, edgecolors='w', label='setosa')

# Scatter plot para la especie 'virginica'
virginica = plt.scatter(iris[iris['species'] == 'virginica']['sepal_length'], iris[iris['species'] == 'virginica']['sepal_width'], c='C1', cmap='viridis', s=50, alpha=0.7, edgecolors='w', label='virginica')

# Scatter plot para la especie 'versicolor'
versicolor = plt.scatter(iris[iris['species'] == 'versicolor']['sepal_length'], iris[iris['species'] == 'versicolor']['sepal_width'], c='C2', cmap='viridis', s=50, alpha=0.7, edgecolors='w', label='versicolor')

# Añadimos etiquetas y título
plt.xlabel('Longitud del Sépalo')
plt.ylabel('Ancho del Sépalo')
plt.title('Longitud y Ancho del Sépalo')

# Mostrar leyenda
plt.legend(loc='lower right', title='Especies')

# Mostrar el gráfico
plt.show()

y la segunda sería creando una variable adicional:

# Cargamos los datos Iris

# Mapeamos los nombres de especies a números para la coloración
species_mapping = {'setosa': 0, 'versicolor': 1, 'virginica': 2}
iris['species_encoded'] = iris['species'].map(species_mapping)

# Creamos el scatter plot
plt.figure(figsize=(8, 6))
scatter = plt.scatter(iris['sepal_length'], iris['sepal_width'], c=iris['species_encoded'], cmap='viridis', s=50, alpha=0.7, edgecolors='w')

# Añadimos etiquetas y título
plt.xlabel('Longitud del Sépalo')
plt.ylabel('Ancho del Sépalo')
plt.title('Longitud y Ancho del Sépalo')

# Obtenemos los colores de la leyenda
colors = [scatter.cmap(scatter.norm(value)) for value in range(len(species_mapping))]

# Creamos la leyenda
legend = plt.legend(handles=scatter.legend_elements()[0], labels=species_mapping.keys(), loc='lower right', title='Especies')

# Asignamos los colores a los puntos de la leyenda
for handle, color in zip(legend.legendHandles, colors):
    handle.set_color(color)

# Mostramos el gráfico
plt.grid(True)
plt.show()

Ejercicio 2: Usando la base de datos penguins de seaborn crea el siguiente gráfico de dispersión.

4.8.2.3 Gráfico de barras

# Datos
categories = ['0-20', '21-40', '41-60', '61-80', '81+']
women_counts = [50, 120, 80, 30, 10]
men_counts = [40, 100, 70, 25, 5]
color_above = 'lightcoral'
color_below = 'lightblue'
offset = 5  # Offset para las barras arriba del eje x

# Creamos el gráfico de barras
plt.figure(figsize=(10, 6))

# Barras superiores (mujeres)
plt.bar(categories, women_counts, color=color_above, label='Mujeres')

# Barras inferiores (hombres)
plt.bar(categories, [-count for count in men_counts], color=color_below, label='Hombres')

# Añadimos etiquetas y título
plt.xlabel('Rango de Edad')
plt.ylabel('Cantidad')
plt.title('Cantidad de Mujeres y Hombres por Rango de Edad en un Centro de Convenciones')

# Añadimos la leyenda
plt.legend()

# Mostramos el gráfico
plt.grid(True)
plt.show()

Ejercicio: Usando la base de datos penguins de seaborn, crea un gráfico de barras que muestre la cantidad promedio de pingüinos de cada especie en la base de datos.

Nota: Si queremos usar la función de matplotlib, para lograr el mismo resultado debemos primero calcular el promedio de las longitudes de los picos y realizar el plot de esos datos. Una opción para hacer esto es la siguiente:

# Calcular el promedio de la longitud del pico por especie
average_bill_length = penguins.groupby('species')['bill_length_mm'].mean()

# Extraer las especies y las longitudes promedio del pico
species = average_bill_length.index
average_lengths = average_bill_length.values

4.8.2.4 Gráfico de pastel

# Datos
edades = ['0-20 años', '21-40 años', '41-60 años', '61-80 años', '81+ años']
cantidad_personas = [25, 50, 40, 20, 5]  # Número de personas en cada grupo de edad

# Colores para cada categoría de edad
colors = ['lightblue', 'lightgreen', 'lightcoral', 'lightyellow', 'lightpink']

# Creamos el pie chart
plt.figure(figsize=(8, 8))
plt.pie(cantidad_personas, labels=edades, autopct='%1.1f%%', startangle=140, colors=colors)
## ([<matplotlib.patches.Wedge object at 0x0000012D3722EBD0>, <matplotlib.patches.Wedge object at 0x0000012D3722DBD0>, <matplotlib.patches.Wedge object at 0x0000012D37242550>, <matplotlib.patches.Wedge object at 0x0000012D3722E3D0>, <matplotlib.patches.Wedge object at 0x0000012D36F1A510>], [Text(-1.0896731958656476, 0.1503739545664288, '0-20 años'), Text(-0.027423702079068427, -1.099658101668095, '21-40 años'), Text(1.0026563830822577, 0.452415934140703, '41-60 años'), Text(-0.21796095220362108, 1.078189697277103, '61-80 años'), Text(-0.7581843362242401, 0.7969670710287902, '81+ años')], [Text(-0.5943671977448985, 0.08202215703623389, '17.9%'), Text(-0.01495838295221914, -0.5998135100007791, '35.7%'), Text(0.5469034816812314, 0.24677232771311072, '28.6%'), Text(-0.11888779211106602, 0.5881034712420561, '14.3%'), Text(-0.41355509248594907, 0.4347093114702491, '3.6%')])
# Añadimos título
plt.title('Distribución de Grupos de Personas por Edad en una Conferencia')

# Mostramos el gráfico
plt.axis('equal')  # Para asegurar que el gráfico sea un círculo
## (-1.0999983812882443, 1.0999999132932203, -1.0999999321961162, 1.0999989838375972)
plt.show()

Para cambiar la paleta de colores se puede agregar la instrucción colors=plt.cm.Accent.colors, por ejemplo:

plt.pie(cantidad_personas, labels=edades, autopct='%1.1f%%', startangle=140, colors=plt.cm.Accent.colors)

Ejercicio: Usando la base de datos penguins de seaborn, crea un gráfico de pastel que muestre la proporción de cada especie de pingüino en la base de datos.

Nota: En este caso, seaborn no tiene una función para hacer gráficos de pastel, por lo tanto para hacerlo con matplotlib deben manipular la base de datos para obtener el recuento de cuantos pinguinos hay por especie, obtener los labels y los tamaños de estos subconjuntos. Una opción es usar la opción penguins['species'].value_counts() y después extraer los labels y tamaños.

4.8.2.5 Boxplot

# Cargamos los datos Iris
iris = sns.load_dataset('iris')

# Creamos el boxplot
plt.figure(figsize=(8, 6))
sns.boxplot(x='species', y='sepal_length', data=iris)

# Añadimos etiquetas y título
plt.xlabel('Especies')
plt.ylabel('Longitud del Sépalo')
plt.title('Boxplot de la longitud del Sépalo para cada especie')

# Mostramos el boxplot
plt.show()

Ejercicio: Usando la base de datos penguins de seaborn, crea un boxplot que compare las longitudes del pico para cada especie de pingüino en la base de datos.

Nota: matplotib no tiene la funcionalidad de eliminar los datos faltantes de una base de datos, si intentan hacer el plot directo con matplolib, para dos especies no va a salir el plot debido a que cuenta con datos faltantes. En seaborn se puede crear el plot usando sns.boxplot(data=, x=, y=). Para hacer el plot con matplotlib, primero se deben eliminar los NA, una forma es con:

penguins_clean = penguins.dropna(subset=['bill_length_mm'])
species = penguins_clean['species']
bill_length = penguins_clean['bill_length_mm']

# Paleta de colores 
colors = sns.color_palette("colorblind")

# Crear el boxplot
plt.figure(figsize=(8, 6))
box = plt.boxplot([bill_length[species == 'Adelie'],
                   bill_length[species == 'Chinstrap'],
                   bill_length[species == 'Gentoo']],
                  labels=['Adelie', 'Chinstrap', 'Gentoo'],
                  patch_artist=True)  # Para que los boxes sean de colores

# Iterar sobre cada caja y establecer el color
for patch, color in zip(box['boxes'], colors):
    patch.set_facecolor(color)

4.8.2.6 Histograma

# Cargar el conjunto de datos Iris
iris = sns.load_dataset('iris')

# Especie específica para la que queremos crear el histograma
especie = 'setosa'

# Filtrar los datos para la especie específica
datos_especie = iris[iris['species'] == especie]['sepal_length']

# Crear el histograma
plt.figure(figsize=(8, 6))
plt.hist(datos_especie, bins=10, color='skyblue', edgecolor='black', alpha=0.7)

# Añadir etiquetas y título
plt.xlabel('Longitud del Sépalo')
plt.ylabel('Frecuencia')
plt.title(f'Histograma de Longitud del Sépalo para la Especie {especie.capitalize()}')

# Mostrar el histograma
plt.show()

Ejercicio: Usando la base de datos penguins de seaborn, crea un histograma que muestre la distribución de las longitudes del pico para todas las especies de pingüinos.

Nota: En seaborn el comando para realizar un histograma es sns.histplot(data=, x=, bins=n, kde=False, color=, edgecolor=).

4.8.2.7 Contour plot

# Creamos datos para el Contour Plot
x = np.linspace(-3, 3, 100)
y = np.linspace(-3, 3, 100)
X, Y = np.meshgrid(x, y)
Z = np.sin(X) * np.cos(Y)

# Creamos el Contour Plot
plt.figure(figsize=(8, 6))
contour = plt.contour(X, Y, Z, cmap='viridis')

# Etiquetamos las líneas de contorno
plt.clabel(contour, inline=True, fontsize=8)

# Añadimos etiquetas y título
plt.xlabel('Eje X')
plt.ylabel('Eje Y')
plt.title('Contour Plot de sin(X) * cos(Y) con Etiquetas')

# Mostramos el gráfico
plt.grid(True)
plt.show()

4.8.2.8 Imshow

En Matplotlib, imshow() se utiliza para mostrar una imagen (2D o 3D) representada como una matriz. Toma como entrada una matriz 2D o 3D donde los valores representan intensidades de píxeles o valores de color.

# Creamos una matriz de ejemplo
data = np.random.rand(10, 10)

# Mostramos la matriz como una imagen utilizando imshow
plt.imshow(data, cmap='viridis')

# Añadimos una barra de color para mostrar la correspondencia entre valores y colores
plt.colorbar()
## <matplotlib.colorbar.Colorbar object at 0x0000012D3DC19F50>
# Añadimos etiquetas y título
plt.xlabel('X')
plt.ylabel('Y')
plt.title('Imshow()')

# Mostramos la gráfica
plt.show()

Se puede ocupar para hacer mapas de calor.

Ejercicio: Supongamos que tenemos la siguiente función:

\[ f(x, y) = \sin(x) \cdot \cos(y) \]

  1. Generar una cuadrícula de valores para \(x\) y \(y\) en el rango de \([-2\pi, 2\pi]\).

  2. Calcular los valores de la función \(f(x, y)\) para cada par de puntos \((x, y)\).

  3. Visualizar la función utilizando imshow para mostrar la función como una imagen y contour para mostrar las curvas de nivel.

  4. Tu resultado debe ser similar al siguiente:

## <matplotlib.colorbar.Colorbar object at 0x0000012D3DC19E50>
## <matplotlib.colorbar.Colorbar object at 0x0000012D3E6BE5D0>

4.9 Data frames con pandas

Pandas es una de las bibliotecas más utilizadas en Python para la manipulación y análisis de datos. Uno de sus objetos principales es el DataFrame, que es una estructura de datos tabular similar a una hoja de cálculo de Excel.

Para instalarlo realizamos lo siguiente:

pip install pandas

Luego, para importarlo

import pandas as pd

Un DataFrame se puede crear de varias formas. Aquí hay algunas de las más comunes:

  1. Desde un diccionario:
data = {'Nombre': ['Laura', 'Ximena', 'Cesar', 'David'],
        'Edad': [25, 30, 35, 40],
        'Ciudad': ['Morelia', 'CDMX', 'Xalapa', 'Toluca']}

df = pd.DataFrame(data)
print(df)
##    Nombre  Edad   Ciudad
## 0   Laura    25  Morelia
## 1  Ximena    30     CDMX
## 2   Cesar    35   Xalapa
## 3   David    40   Toluca
  1. Desde una lista de listas:
data = [['Laura', 25, 'Morelia'],
        ['Ximena', 30, 'CDMX'],
        ['Cesar', 35, 'Xalapa'],
        ['David', 40, 'Toluca']]

df = pd.DataFrame(data, columns=['Nombre', 'Edad', 'Ciudad'])
print(df)
##    Nombre  Edad   Ciudad
## 0   Laura    25  Morelia
## 1  Ximena    30     CDMX
## 2   Cesar    35   Xalapa
## 3   David    40   Toluca
  1. Desde un archivo CSV: Supongamos que tenemos un DataFrame llamado ‘data’, vamos a guardarlo como sigue:
# index=False evita que se escriba el índice del DataFrame en el archivo
df = pd.DataFrame(data, index = ['C1', 'C2', 'C3', 'C4'])
df.to_csv('diccionario.csv', index=False)  

Entonces, para leerlo:

df = pd.read_csv('archivo_prueba.csv')
print(df)

Una vez que tenemos un DataFrame, podemos explorar los datos utilizando varias funciones y atributos:

# Obtener las primeras filas
print(df.head())

# Obtener las últimas filas
print(df.tail())

# Obtener información sobre el DataFrame
print(df.info())

# Obtener estadísticas descriptivas
print(df.describe())

Para acceder y manipular los datos en un DataFrame, tenemos varias formas.

  1. Acceder a columnas:
data = {'Nombre': ['Laura', 'Ximena', 'Cesar', 'David'],
        'Edad': [25, 30, 35, 40],
        'Ciudad': ['Morelia', 'CDMX', 'Xalapa', 'Toluca']}

df = pd.DataFrame(data, index = ['C1', 'C2', 'C3', 'C4'])

# Acceder a una columna
print(df['Nombre'])
# Acceder a una columna
print(df['Nombre'])

# Acceder a múltiples columnas
print(df[['Nombre', 'Edad']])
  1. Acceder a filas:
# Acceder a una fila por índice
print(df.iloc[0])
## 0      Laura
## 1         25
## 2    Morelia
## Name: C1, dtype: object
# Acceder a varias filas
print(df.iloc[1:3])
##          0   1       2
## C2  Ximena  30    CDMX
## C3   Cesar  35  Xalapa

En el caso de que nuestras filas tengan índice podemos realizar lo siguiente también:

# Acceder a una fila por su índice especifico

print(df.loc['C1'])
## 0      Laura
## 1         25
## 2    Morelia
## Name: C1, dtype: object

# Si no tiene nombre de índice entonces sería:
# df.loc[1]
  1. Seleccionar subconjuntos (filas y columnas):
print(df.iloc[0:2, 0:2])
##          0   1
## C1   Laura  25
## C2  Ximena  30
  1. Para añadir columnas:
# Añadir una nueva columna
df['Género'] = ['F', 'M', 'M', 'M']
print(df)
##          0   1        2 Género
## C1   Laura  25  Morelia      F
## C2  Ximena  30     CDMX      M
## C3   Cesar  35   Xalapa      M
## C4   David  40   Toluca      M
  1. Filtrar datos:
# Filtrar filas basadas en una condición
print(df[df['Edad'] > 30])

# Filtrar filas basadas en múltiples condiciones
print(df[(df['Edad'] > 30) & (df['Ciudad'] == 'CDMX')])
  1. Ordenar datos:
# Ordenar DataFrame por una columna
print(df.sort_values(by='Edad'))

# Ordenar DataFrame en orden descendente
print(df.sort_values(by='Edad', ascending=False))

Ejercicios:

  1. Crea un DataFrame con información sobre empleados, incluyendo Nombre, Edad y Salario. Muestra las primeras 5 filas.
  2. Añade una nueva columna llamada ‘Departamento’ al DataFrame de empleados.
  3. Encuentra el empleado más joven en el DataFrame.
  4. Filtra los empleados que tienen un salario mayor a $50000.
  5. Ordena los empleados por salario en orden descendente.

4.9.1 Funciones para manipular dataframes

Vamos a trabajar lo siguiente con la base de datos de pinguinos.

Primero, cargamos la base de datos ‘penguins’ de Seaborn:

penguins = sns.load_dataset('penguins')
  1. Agrupar por una columna y calcular la suma de otra columna:
suma_por_especie = penguins.groupby('species')['bill_length_mm'].sum()
suma_por_especie
## species
## Adelie       5857.5
## Chinstrap    3320.7
## Gentoo       5843.1
## Name: bill_length_mm, dtype: float64

Aquí, estamos agrupando los datos por la columna ‘species’ y luego calculando la suma de los valores en la columna ‘bill_length_mm’ para cada grupo de especie. El resultado es una Serie que contiene la suma de las longitudes del pico para cada especie.

Podemos agrupar también por múltiples columnas en Pandas y luego aplicar funciones de agregación para obtener información sobre cada grupo. Para obtener el conteo de cada sexo por especie en la base de datos ‘penguins’:

# Agrupar por especie y sexo y contar el número de registros en cada grupo
conteos = penguins.groupby(['species', 'sex']).size()

conteos = penguins.groupby(['species', 'sex']).size().reset_index(name='conteo')

conteos
##      species     sex  conteo
## 0     Adelie  Female      73
## 1     Adelie    Male      73
## 2  Chinstrap  Female      34
## 3  Chinstrap    Male      34
## 4     Gentoo  Female      58
## 5     Gentoo    Male      61

reset_index(name='conteo') se utiliza para renombrar la columna que contiene los conteos como ‘conteo’. La función size() en Pandas devuelve una Serie que contiene el tamaño de cada grupo en el objeto agrupado. Cuando se aplica a un DataFrame agrupado por una o más columnas, como en penguins.groupby([‘species’, ‘sex’]), size() cuenta el número de filas en cada grupo resultante.

  1. Agrupar por múltiples columnas y calcular la media de otra columna:
promedio_por_especie_island = penguins.groupby(['species', 'island'])['bill_length_mm'].mean()

Esta línea agrupa los datos por las columnas ‘species’ y ‘island’ y luego calcula la media de los valores en la columna ‘bill_length_mm’ para cada combinación de especie e isla. El resultado es una Serie que contiene el promedio de las longitudes del pico para cada especie en cada isla.

  1. Calcular el máximo de una columna:
maximo_longitud_pico = penguins['bill_length_mm'].max()
maximo_longitud_pico
## 59.6
  1. Calcular el mínimo de una columna:
minimo_longitud_pico = penguins['bill_length_mm'].min()
minimo_longitud_pico
## 32.1
  1. Calcular el valor máximo de cada columna:
# seleccionamos solo las columnas numericas
maximos_por_columna = penguins[['bill_length_mm', 'bill_depth_mm', 'flipper_length_mm', 'body_mass_g']].max()
maximos_por_columna
## bill_length_mm         59.6
## bill_depth_mm          21.5
## flipper_length_mm     231.0
## body_mass_g          6300.0
## dtype: float64
  1. Calcular el valor mínimo de cada columna:
minimos_por_columna = penguins[['bill_length_mm', 'bill_depth_mm', 'flipper_length_mm', 'body_mass_g']].min()
minimos_por_columna
## bill_length_mm         32.1
## bill_depth_mm          13.1
## flipper_length_mm     172.0
## body_mass_g          2700.0
## dtype: float64
  1. Calcular la media de una columna:
media_longitud_pico = penguins['bill_length_mm'].mean()
media_longitud_pico
## 43.9219298245614
  1. Calcular la mediana de una columna:
mediana_longitud_pico = penguins['bill_length_mm'].median()
mediana_longitud_pico
## 44.45
  1. Calcular la desviación estándar de una columna:
desviacion_estandar_longitud_pico = penguins['bill_length_mm'].std()
desviacion_estandar_longitud_pico
## 5.459583713926532
  1. Calcular el conteo de valores no nulos en una columna:
conteo_valores_no_nulos_longitud_pico = penguins['bill_length_mm'].count()
conteo_valores_no_nulos_longitud_pico
## 342

Estamos contando el número de valores no nulos en la columna ‘bill_length_mm’, que representa el número de registros en el DataFrame ‘penguins’ donde la longitud del pico está registrada.

  1. Obtener la forma del data frame:
penguins.shape
## (344, 7)
  1. Crear una nueva columna calculada a partir de columnas existentes: Supongamos que queremos calcular el índice de masa corporal (IMC) para cada pingüino en función de su peso (en kg) y su altura (en metros). Podemos agregar una nueva columna llamada ‘bmi’ que contenga el IMC calculado utilizando la fórmula IMC = peso / (altura\(^2\)).
# Calcular el índice de masa corporal (IMC)
penguins['bmi'] = penguins['body_mass_g'] / ((penguins['bill_length_mm'] / 100) ** 2)
  1. Crear una nueva columna basada en condiciones: Podemos agregar una nueva columna llamada ‘is_adult’ que indique si un pingüino es adulto o no, basado en si su edad es mayor o igual a 2 años.
# Crear una nueva columna 'is_adult'
penguins['is_adult'] = penguins['age'] >= 2
  1. Crear una nueva columna combinando valores de otras columnas: Podemos combinar el nombre de la especie y el sexo para crear una nueva columna llamada ‘species_sex’.
# Crear una nueva columna 'species_sex'
penguins['species_sex'] = penguins['species'] + '_' + penguins['sex']
  1. Crear una nueva columna utilizando el método assign: Podemos usar el método assign para agregar múltiples columnas de una vez, calculadas a partir de columnas existentes.
# Agregar nuevas columnas usando el método 'assign'
penguins = penguins.assign(
  bill_ratio = penguins['bill_length_mm'] / penguins['bill_depth_mm'],
  flipper_ratio = penguins['flipper_length_mm'] / penguins['bill_length_mm']
)

Extra) Crear una nueva columna utilizando una función personalizada: Podemos definir una función personalizada para asignar una categoría de tamaño de pingüino (pequeño, mediano, grande) en función de su peso y usarla para crear una nueva columna llamada ‘size_category’.

# Función para asignar la categoría de tamaño del pingüino
def size_category(weight):
  if weight < 3000:
    return 'small'
  elif weight < 5000:
    return 'medium'
  else:
    return 'large'

# Crear una nueva columna 'size_category'
penguins['size_category'] = penguins['body_mass_g'].apply(size_category)

4.9.2 pivot y pivot_table

pivot y pivot_table son dos métodos en Pandas que te permiten reorganizar y resumir datos en un DataFrame. Estos métodos son útiles para transformar la estructura de tus datos para un análisis más conveniente y presentación visual.

1)pivot: El método pivot te permite reorganizar datos en función de los valores de las columnas.

# Crear un DataFrame de ejemplo
data = {
    'fecha': ['2022-01-01', '2022-01-01', '2022-01-02', '2022-01-02'],
    'ciudad': ['A', 'B', 'A', 'B'],
    'temperatura': [25, 28, 23, 26]
}
df = pd.DataFrame(data)
print(df)
##         fecha ciudad  temperatura
## 0  2022-01-01      A           25
## 1  2022-01-01      B           28
## 2  2022-01-02      A           23
## 3  2022-01-02      B           26
# Usar pivot para reorganizar los datos
pivot_df = df.pivot(index='fecha', columns='ciudad', values='temperatura')
print(pivot_df)
## ciudad       A   B
## fecha             
## 2022-01-01  25  28
## 2022-01-02  23  26

2). pivot_table: El método pivot_table es similar a pivot, pero te permite agregar y resumir datos.

df2 = pd.DataFrame({"A": ["foo", "foo", "foo", "foo", "foo",
                         "bar", "bar", "bar", "bar"],
                    "B": ["one", "one", "one", "two", "two",
                          "one", "one", "two", "two"],
                    "C": ["small", "large", "large", "small",
                          "small", "large", "small", "small",
                          "large"],
                    "D": [1, 2, 2, 3, 3, 4, 5, 6, 7],
                    "E": [2, 4, 5, 5, 6, 6, 8, 9, 9]})
print(df2)

# Usar pivot_table para calcular el promedio de temperatura por ciudad
pivot_table_df = df2.pivot_table(df2, values='D', index=['A', 'B'],
                       columns=['C'], aggfunc="sum")
print(pivot_table_df)

En este ejemplo, además de reorganizar los datos, estamos calculando la suma.

Ejercicio 1: Carga el conjunto de datos ‘titanic’ de Seaborn y utiliza pivot para reorganizar los datos de manera que las filas representen la clase de pasajeros y las columnas representen el sexo de los pasajeros, con los valores siendo la tarifa media pagada por cada grupo.

Ejercicio 2: Utiliza pivot_table para calcular la cantidad total de sobrevivientes agrupados por clase y género en el conjunto de datos ‘titanic’.

4.9.3 Plots en pandas

# Crear un DataFrame de ejemplo
data = pd.DataFrame({
    'A': np.random.randn(100),
    'B': np.random.rand(100) * 100,
    'C': np.random.randint(1, 5, size=100),
    'D': np.random.choice(['X', 'Y', 'Z'], size=100)
})
# Gráfico de línea
data.plot(kind='line', figsize=(10, 6))
plt.title('Gráfico de Línea')
plt.xlabel('Índice')
plt.ylabel('Valores')
plt.show()

# Gráfico de barras verticales
data['C'].value_counts().sort_index().plot(kind='bar', figsize=(10, 6))
plt.title('Gráfico de Barras Verticales')
plt.xlabel('Valores')
plt.ylabel('Frecuencia')
plt.show()

# Gráfico de barras horizontales
data['D'].value_counts().plot(kind='barh', figsize=(10, 6))
plt.title('Gráfico de Barras Horizontales')
plt.xlabel('Frecuencia')
plt.ylabel('Valores')
plt.show()

# Histograma
data['A'].plot(kind='hist', bins=20, figsize=(10, 6))
plt.title('Histograma')
plt.xlabel('Valores')
plt.ylabel('Frecuencia')
plt.show()

# Diagrama de caja (boxplot)
data[['A', 'B']].plot(kind='box', figsize=(10, 6))
plt.title('Diagrama de Caja')
plt.ylabel('Valores')
plt.show()

# Estimación de densidad de kernel (KDE)
data['B'].plot(kind='kde', figsize=(10, 6))
plt.title('Estimación de Densidad de Kernel (KDE)')
plt.xlabel('Valores')
plt.ylabel('Densidad')
plt.show()

# Histograma de densidad
data['B'].plot(kind='density', figsize=(10, 6))
plt.title('Histograma de Densidad')
plt.xlabel('Valores')
plt.ylabel('Densidad')
plt.show()

# Asegurarnos de que los valores sean no negativos
data_non_negative = data[['A', 'B']].clip(lower=0)

# Gráfico de área
data_non_negative.plot(kind='area', stacked=False, figsize=(10, 6))
plt.title('Gráfico de Área')
plt.xlabel('Índice')
plt.ylabel('Valores')
plt.show()

# Gráfico de dispersión
data.plot(kind='scatter', x='A', y='B', figsize=(10, 6))
plt.title('Gráfico de Dispersión')
plt.xlabel('A')
plt.ylabel('B')
plt.show()

4.10 Análisis exploratorio de bases de datos

Link a diapositivas

4.11 Funciones y scripts

4.11.1 Funciones

Las funciones son bloques de código que realizan una tarea específica y pueden ser reutilizadas en múltiples partes de un programa. En Python, las funciones se definen utilizando la palabra clave def.

def saludar(nombre):
    print("Hola,", nombre)

saludar("Roberta")
## Hola, Roberta

Las funciones pueden aceptar parámetros, que son valores que la función espera recibir cuando es llamada. Los valores pasados a una función se llaman argumentos. Existen argumentos que se pueden dejar por default u opcionales.

def sumar(a,b):
  return a + b

resultado = sumar(189,278)

print("La suma es:", resultado)
## La suma es: 467

Se pueden definir valores predeterminados para los argumentos de una función en Python. Estos valores se utilizarán si el usuario no proporciona un valor específico para el argumento al llamar a la función. Sin embargo, el usuario puede sobrescribir el valor predeterminado proporcionando un valor diferente al llamar a la función.

def saludar(nombre="usuario"):
    print("Hola,", nombre)

# Llamada a la función sin argumentos
saludar()  # Salida: Hola, usuario
## Hola, usuario
# Llamada a la función con un argumento
saludar("Roberta")  # Salida: Hola, Roberta
## Hola, Roberta

También se pueden definir argumentos opcionales en Python utilizando el valor None como un valor predeterminado para el argumento. Luego, dentro de la función, puedes verificar si se ha proporcionado un valor para ese argumento y actuar en consecuencia.

def saludar(nombre=None):
    if nombre is None:
        print("Hola, ¿cómo estás?")
    else:
        print("Hola,", nombre)

# Llamada a la función sin argumentos
saludar()  # Salida: Hola, ¿cómo estás?
## Hola, ¿cómo estás?
# Llamada a la función con un argumento
saludar("Roberta")  # Salida: Hola, Roberta
## Hola, Roberta

Un punto a tener en cuenta cuando se escriben funciones es que las variables que se definen dentro de una función solo se conocen dentro de la función. Por ejemplo:

def imprimir_mensaje():
    mensaje = "Hola, Roberta!"
    print(mensaje)

imprimir_mensaje()
print(mensaje)

Otro punto importante a la hora de realizar funciones es la documentación.

def boxplot_especie(data, especie, variable):
    """
    Crea un boxplot para una especie específica en un conjunto de datos,
    utilizando una variable específica.

    Parámetros:
    - data: DataFrame, el conjunto de datos que contiene la información.
    - especie: str, la especie para la cual se desea generar el boxplot.
    - variable: str, la variable numérica para la cual se desea generar el boxplot.

    Si la columna 'species' está presente en el DataFrame 'data', la función
    genera un boxplot para la especie especificada utilizando la variable numérica
    especificada. De lo contrario, imprime un mensaje indicando que no se pudo
    determinar el tipo de base de datos.

    Si la especie o la variable no se encuentran en los datos, la función imprime
    un mensaje indicando que no se pudo encontrar la especie o la variable.

    Ejemplos de uso:
    - boxplot_especie(iris_data, 'setosa', 'petal_length')
    - boxplot_especie(penguins_data, 'Adelie', 'flipper_length_mm')
    """
    # Verificar si existe la columna 'species'
    if 'species' in data.columns:
        especies_disponibles = data['species'].unique()
        if especie not in especies_disponibles:
            print(f"Especie '{especie}' no encontrada en la base de datos.")
            return
        # Verificar si la variable especificada está disponible
        if variable not in data.columns:
            print(f"Variable '{variable}' no encontrada en la base de datos.")
            return
        # Crear el boxplot
        sns.boxplot(x='species', y=variable, data=data[data['species'] == especie])
        plt.xlabel('Especie')
        plt.ylabel(f'{variable}')
        plt.title(f'Boxplot de la variable {variable} para la especie {especie}')
    else:
        print("No se pudo determinar el tipo de base de datos.")
        return
    plt.show()
# Ejemplo de uso con la base de datos Iris
iris_data = sns.load_dataset('iris')
boxplot_especie(iris_data, 'setosa', 'petal_lengt')
## Variable 'petal_lengt' no encontrada en la base de datos.
# Ejemplo de uso con la base de datos Penguins
penguins_data = sns.load_dataset('penguins')
boxplot_especie(penguins_data, 'Adele', 'flipper_length_mm')
## Especie 'Adele' no encontrada en la base de datos.

Una vez que tenemos funciones, podemos automatizar procesos. De esta forma hacemos que nuestro código sea más eficiente y menos propenso a errores. Esto es especialmente útil cuando tenemos un gran número de especies o cuando trabajamos con conjuntos de datos más grandes en los que escribir llamadas individuales a funciones para cada observación sería tedioso y propenso a errores.

# Cargar la base de datos Iris
iris_data = sns.load_dataset('iris')

# Obtener las especies únicas en la base de datos
especies_unicas = iris_data['species'].unique()

# Generar boxplots para cada especie en un bucle for
for especie in especies_unicas:
    boxplot_especie(iris_data, especie, 'petal_length')

Cuando estamos realizando funciones, es importante que sean funciones legibles, usar nombres descriptivos, documentarlas, etc. Por ejemplo:

import math

def desv(val):
    me = sum(val) / len(val)
    s = sum((x - me) ** 2 for x in val)
    v = s / len(val)
    d = math.sqrt(v)
    return d

# Ejemplo de uso
v = [2, 4, 4, 4, 5, 5, 7, 9]
print("Desviación estándar:", desv(v))
## Desviación estándar: 2.0

Esa misma función pero legible:

import math

def calcular_desviacion_estandar(valores):
    """
    Calcula la desviación estándar de una lista de valores.

    Parámetros:
    - valores: list[float], una lista de valores numéricos.

    Retorna:
    - float, la desviación estándar de los valores.

    """
    # Calcular la media de los valores
    media = sum(valores) / len(valores)
    
    # Calcular la suma de los cuadrados de las diferencias entre cada valor y la media
    suma_cuadrados_diferencias = sum((x - media) ** 2 for x in valores)
    
    # Calcular la varianza
    varianza = suma_cuadrados_diferencias / len(valores)
    
    # Calcular la desviación estándar como la raíz cuadrada de la varianza
    desviacion_estandar = math.sqrt(varianza)
    
    return desviacion_estandar

# Ejemplo de uso
valores = [2, 4, 4, 4, 5, 5, 7, 9]
print("Desviación estándar:", calcular_desviacion_estandar(valores))
## Desviación estándar: 2.0

Existen diferentes formas de obtener resultados de una función.

  • print: es una función incorporada en Python que se utiliza para mostrar información en la consola o en la salida estándar. Cuando llamas a print dentro de una función, la información se imprime en la consola pero no se devuelve al código que llamó a la función. print es útil para la depuración y la visualización de datos, pero no proporciona un valor específico que pueda ser utilizado por otras partes del programa. Por ejemplo:
def saludar(nombre):
    print("Hola,", nombre)

saludar("Roberta")
## Hola, Roberta

En este ejemplo, la función saludar imprime un saludo utilizando print, pero no devuelve ningún valor. La salida se muestra en la consola, pero no se asigna a ninguna variable ni se utiliza más adelante en el programa.

  • return: es una palabra clave en Python que se utiliza para devolver un valor específico desde una función al código que la llamó. Cuando una función utiliza return, termina la ejecución de la función y devuelve el valor especificado al lugar donde se llamó la función. Esto permite utilizar el resultado de la función en otras partes del programa. Por ejemplo:
def sumar(a, b):
    return a + b

resultado = sumar(3, 5)
print(resultado)
## 8

En este ejemplo, la función sumar devuelve la suma de dos números utilizando return. El valor devuelto (la suma de los números) se asigna a la variable resultado y luego se imprime en la consola.

La diferencia principal entre print y return es que print muestra información en la consola pero no devuelve ningún valor, mientras que return devuelve un valor específico desde una función para que pueda ser utilizado por otras partes del programa.

Ejercicios:

4.11.2 Scripts

Un script en Python es un archivo que contiene código Python que puede ser ejecutado. Los scripts nos permiten organizar nuestro código en archivos separados y reutilizables.

Supongamos que queremos crear un script que salude a un usuario con su nombre. Podemos escribir el siguiente código en un archivo llamado saludo.py:

# Contenido del archivo saludo.py
def saludar(nombre):
    print("Hola,", nombre)

nombre_usuario = input("Por favor, ingresa tu nombre: ")
saludar(nombre_usuario)

Para ejecutar este script, podemos abrir una terminal, navegar hasta la ubicación del archivo saludo.py y ejecutar el siguiente comando:

python saludo.py

4.11.3 Test

Una vez que tenemos funciones, también queremos realizar tests para ver si están realizando correctamente lo que queremos las funciones. Una opción es la siguiente:

def sumar(a, b):
    return a + b
# Test para verificar la función sumar
def test_sumar():
    # Verificar suma de números positivos
    resultado = sumar(3, 5)
    assert resultado == 8, f"La suma de 3 y 5 debería ser 8, pero obtuvimos {resultado}"

    # Verificar suma de números negativos
    resultado = sumar(-3, -5)
    assert resultado == -8, f"La suma de -3 y -5 debería ser -8, pero obtuvimos {resultado}"

    # Verificar suma de un número positivo y otro negativo
    resultado = sumar(3, -5)
    assert resultado == -2, f"La suma de 3 y -5 debería ser -2, pero obtuvimos {resultado}"

# Ejecutar el test
test_sumar()
# Test para verificar la función sumar
def test_sumar_incorrecto():
    # Verificar suma de números positivos
    resultado = sumar(3, 5)
    assert resultado == 7, f"La suma de 3 y 5 debería ser 8, pero obtuvimos {resultado}"

    # Verificar suma de números negativos
    resultado = sumar(-3, -5)
    assert resultado == -8, f"La suma de -3 y -5 debería ser -8, pero obtuvimos {resultado}"

    # Verificar suma de un número positivo y otro negativo
    resultado = sumar(3, -5)
    assert resultado == -2, f"La suma de 3 y -5 debería ser -2, pero obtuvimos {resultado}"

# Ejecutar el test
test_sumar_incorrecto()

Como no obtenemos ningún error quiere decir que se cumplieron todos los test. Modifiquemos uno de los resultados para ver que sucede cuando no se cumple el test.

Dos paquetes que nos ayudan a realizar test son unittest y pytest. En pytest, cada función a realizarle un test debe llevar el nombre test_funcion. Cada test verifica una solo característica de nuestro código y revisa usando assert.

Vamos a crear un script en python llamado test_suma.py, en el vamos a escribir la función suma que teníamos y los test que escribimos anteriormente:

# Script.py
def sumar(a, b):
    return a + b

def test_sumar():
    # Verificar suma de números positivos
    resultado = sumar(3, 5)
    assert resultado == 8, f"La suma de 3 y 5 debería ser 8, pero obtuvimos {resultado}"

    # Verificar suma de números negativos
    resultado = sumar(-3, -5)
    assert resultado == -8, f"La suma de -3 y -5 debería ser -8, pero obtuvimos {resultado}"

    # Verificar suma de un número positivo y otro negativo
    resultado = sumar(3, -5)
    assert resultado == -2, f"La suma de 3 y -5 debería ser -2, pero obtuvimos {resultado}"

Para correrlo, vamos a abrir una terminal y vamos a ejecutar:

pytest test_suma.py

Ejercicio 1: Crea un test que verifique dos funciones, una de aritmética básica (igualdad entre dos números y multiplicación entre dos números) y la otra función la longitud de elementos de una lista. Realiza el test de ambas funciones con pytest.

Ejercicio 2: Crea un archivo nuevo para hacer test llamado test_prueba. Realiza una función prueba que verifique que 1+2 es 3. Realiza el test. Ahora prueba con 1.1+2.2 es 3.3, usa pytest.

Con los números flotantes debemos tener cuidado al realizar los test. Una forma de corregir el error que tuvimos es la siguiente.

from math import isclose

def test_floating_point_math():
    assert isclose(1.1+2.2, 3.3)

Otra forma es controlando la tolerancia absoluta:

# pass
assert isclose(1.21, 1.2, abs_tol=0.1)

#fail
assert isclose(1.21, 1.2, abs_tol=0.01)

Y la relativa:

# pass
assert isclose(1.21, 1.2, rel_tol=0.1)

#fail
assert isclose(1.21, 1.2, rel_tol=0.01)

En el siguiente repositorio, se encuentra un muy buen manual de como realizar test con diferentes opciones link.

4.12 Buenas practicas

  1. Sigue las convenciones de estilo de código:
  • Utiliza PEP 8 como guía para escribir código Python limpio y legible.
  • Usa nombres de variables descriptivos y significativos.
  • Usa espacios en blanco de manera consistente para mejorar la legibilidad.
  1. Comenta tu código:
  • Utiliza comentarios para explicar el propósito y la funcionalidad de tu código.
  • Documenta tus funciones utilizando cadenas de documentación (docstrings) para describir lo que hacen, los parámetros que reciben y lo que devuelven.
  1. Escribir funciones y módulos reutilizables:
  • Divide tu código en funciones y módulos pequeños y reutilizables.
  • Evita la repetición de código escribiendo funciones para realizar tareas comunes.
  1. Manejo adecuado de excepciones:
  • Utiliza bloques try-except para manejar excepciones de manera adecuada y evitar que tu programa se bloquee.
  • Especifica claramente qué excepciones estás capturando y maneja las excepciones de manera apropiada.
  1. Optimiza tu código:
  • Utiliza la biblioteca estándar de Python y módulos externos cuando sea posible en lugar de reinventar la rueda.
  • Aprende y utiliza algoritmos y estructuras de datos eficientes para optimizar el rendimiento de tu código.
  1. Escribe pruebas unitarias:
  • Escribe pruebas unitarias para verificar que tu código funcione correctamente en diferentes casos de uso.
  • Utiliza marcos de prueba como unittest o pytest para escribir y ejecutar tus pruebas.
  1. Mantén tu código limpio:
  • Elimina el código muerto y no utilizado para mantener tu código limpio y fácil de entender.
  • Refactoriza tu código cuando sea necesario para mejorar su estructura y claridad.
  1. Usa control de versiones:
  • Utiliza un sistema de control de versiones como Git para gestionar y realizar un seguimiento de los cambios en tu código.
  • Utiliza ramas y etiquetas para organizar tu trabajo y colaborar con otros desarrolladores de manera efectiva.
  1. Aprende continuamente:
  • Mantente al tanto de las mejores prácticas y las novedades en el mundo de Python.
  • Aprende de otros desarrolladores y contribuye a la comunidad de código abierto.

4.13 Procesamiento de alto rendimiento

Numba es una biblioteca de Python que te permite acelerar el rendimiento de tu código mediante la compilación de funciones de Python en código máquina optimizado para CPU o GPU.

Numba proporciona varios decoradores que puedes usar para optimizar tus funciones de Python. Dos de los más comunes son @jit y @njit. @jit (Just-In-Time) compila la función justo antes de ejecutarla, mientras que @njit (Numba JIT) compila la función antes de su primera llamada y la reutiliza en llamadas posteriores.

Nota: Basado en link

Para usar numba, solo se necesita agregar los decoradores. Por ejemplo, supongamos que tenemos la siguiente función que integra.

import numpy as np
import numba

def integrate(a, b, n):
    s = 0.0
    dx = (b - a) / n
    for i in range(n):
        x = a + (i + 0.5) * dx
        y = x ** 4 - 3 * x
        s += y * dx
    return s

Si la ejecutamos:

a = 0
b = 2
n = 1_000_000

plaintime = %timeit -o integrate(a, b, n)

Vemos el tiempo que se lleva en compilarla.

Ahora, hagamos una prueba con numba.

@numba.njit 
def numba_integrate(a, b, n):
    s = 0.0
    dx = (b - a) / n
    for i in range(n):
        x = a + (i + 0.5) * dx
        y = x ** 4 - 3 * x
        s += y * dx
    return s

El decorador hará que la función corra sin necesidad del interprete. Verifiquemos el tiempo de ejecución.

jittedtime = %timeit -o -n 10 numba_integrate(a, b, n)

Realizamos una aserción para verificar que ambos resultados son casi iguales:

np.testing.assert_almost_equal(numba_integrate(a, b, n), integrate(a, b, n))

Calculamos la relación de aceleración comparando los mejores tiempos de ejecución entre la versión original y la versión compilada con Numba:

plaintime.best/jittedtime.best

Cuando decoras una función con @numba.njit, Numba compila la función justo antes de su ejecución. Durante esta compilación, Numba analiza el código de la función y genera un código máquina optimizado para los tipos de datos de los argumentos que se le pasan en esa llamada específica. Esta compilación puede llevar tiempo, especialmente para funciones complejas o con tipos de datos complicados.

Una vez que la función ha sido compilada para un conjunto particular de tipos de datos, Numba almacena en caché el código máquina generado. Esto significa que si la función se llama nuevamente con los mismos tipos de datos, Numba puede reutilizar el código máquina en lugar de tener que compilar la función nuevamente. Esto generalmente resulta en una ejecución mucho más rápida, ya que no se requiere el tiempo de compilación en ese punto.

@numba.njit
def numba_integrate(a, b, n):
    s = 0.0
    dx = (b - a) / n
    for i in range(n):
        x = a + (i + 0.5) * dx
        y = x ** 4 - 3 * x
        s += y * dx
    return s          

# Tiempo de compilación!
start = time.perf_counter()
numba_integrate(a, b, n)
end = time.perf_counter()
print("Tiempo (con compilación) = %0.5fs" %(end - start))

# Re-ejecutar con la versión ya compilada
start = time.perf_counter()
numba_integrate(a, b, n)
end = time.perf_counter()
print("Tiempo (después de compilación) = %0.5fs" %(end - start))

# FUnción original
start = time.perf_counter()
integrate(a, b, n)
end = time.perf_counter()
print("Tiempo (sin numba) = %0.5fs" %(end - start))

¿Cuándo usar Numba?

  1. Funciones con bucles: Numba es especialmente efectivo para optimizar funciones que contienen bucles, ya que puede compilar estos bucles en código de bajo nivel que se ejecuta rápidamente en la CPU o en la GPU.

  2. Funciones con operaciones matemáticas: Si tu código realiza muchas operaciones matemáticas, como sumas, multiplicaciones, exponenciaciones, etc., Numba puede mejorar significativamente su rendimiento al compilar estas operaciones en código máquina optimizado.

  3. Funciones que utilizan NumPy: Numba se integra bien con NumPy y puede mejorar el rendimiento de las funciones que utilizan arrays NumPy. Puede optimizar las operaciones vectorizadas y las funciones de NumPy para que se ejecuten más rápido.

  4. Adecuado para: tuplas, cadenas de texto: Numba puede ser utilizado de manera efectiva en funciones que operan con tuplas y cadenas de texto, ya que puede optimizar las operaciones sobre estos tipos de datos.

  5. No tan adecuado para: objetos, listas de Python, diccionarios de Python: Numba no es tan efectivo en optimizar el rendimiento de funciones que operan con objetos de Python, listas de Python y diccionarios de Python. Esto se debe a que estos tipos de datos son dinámicos y pueden cambiar de tipo durante la ejecución, lo que hace más difícil para Numba aplicar optimizaciones.

4.14 Programación en paralelo

Paralelizar en Python significa ejecutar múltiples tareas simultáneamente, aprovechando así los recursos de hardware, como múltiples núcleos de CPU, para acelerar el procesamiento. Esto se logra dividiendo una tarea en partes más pequeñas y ejecutándolas de forma concurrente en varios procesos o hilos.

Imagina que estás organizando una cena en tu casa y tienes que cocinar varios platos al mismo tiempo para satisfacer los gustos de todos tus invitados. En este contexto, la cocina y la computadora están relacionadas a través de analogías con conceptos clave de la informática:

  1. Proceso: Un proceso es una instancia de un programa que se está ejecutando en la computadora. En el contexto de la cocina, podríamos pensar en un proceso como una receta específica que se está preparando en la cocina. Cada receta (proceso) tiene sus propios ingredientes y pasos de preparación. Así como en la cocina, donde puedes tener varias recetas siendo preparadas al mismo tiempo en diferentes estaciones, en la computadora puedes tener varios procesos ejecutándose simultáneamente.

  2. Hilo: Un hilo es una unidad de computación que se envía a la CPU para su ejecución. En el contexto de la cocina, podríamos comparar un hilo con un chef o un ayudante que realiza una tarea específica dentro de un proceso (receta). Al igual que en una cocina ocupada donde varios chefs y ayudantes trabajan en diferentes tareas al mismo tiempo para preparar una comida completa, en una computadora, varios hilos pueden ejecutarse simultáneamente para completar diferentes partes de un proceso.

  3. Número de núcleos: El número de núcleos en una CPU representa la cantidad de unidades independientes que pueden ejecutar hilos simultáneamente. En el contexto de la cocina, podríamos comparar los núcleos con las estaciones de trabajo en una cocina profesional. Cuantos más núcleos tenga una CPU, más hilos podrán ejecutarse simultáneamente, lo que permite una mayor paralelización y un rendimiento general más rápido.

  4. Memoria: La memoria de la computadora es donde se almacenan y actualizan los datos y variables durante la ejecución de un programa. En la cocina, podríamos pensar en la memoria como el espacio de trabajo donde se colocan los ingredientes, utensilios y platos durante la preparación de una receta. Al igual que una cocina bien organizada facilita el acceso rápido a los ingredientes y utensilios necesarios para preparar una receta, una gestión eficiente de la memoria en la computadora asegura un acceso rápido y eficiente a los datos necesarios para ejecutar un programa.

En este ejemplo de cocina, los “workstations” (cores) representan los núcleos de la CPU, y los “sous chefs” (threads) representan los hilos de ejecución. La tarea de “hacer salsa” implica realizar sub-tareas, una de las cuales es contar todos los tomates en la despensa, tomar \(n\) tomates para la salsa y comunicarle al Chef cuántos tomates quedan en la despensa.

Ahora, supongamos que dos “sous chefs” están haciendo salsa y ejecutan sus tareas al mismo tiempo. Ambos necesitan contar los tomates en la despensa, tomar algunos para la salsa y actualizar la cantidad restante de tomates en la despensa. Aquí es donde surge el problema debido al GIL (Global Interpreter Lock).

El GIL es un mecanismo de protección en el intérprete de Python que evita que múltiples hilos ejecuten código de Python simultáneamente. Esto significa que, aunque tengamos múltiples hilos, solo uno puede ejecutar código Python a la vez. En el ejemplo de la cocina, esto se traduce en que, incluso si tenemos múltiples “sous chefs” (hilos) trabajando simultáneamente, solo uno puede contar los tomates a la vez debido al GIL.

Si dos “sous chefs” ejecutan sus tareas de contar tomates al mismo tiempo, pueden ocurrir problemas. Por ejemplo, si un “sous chef” cuenta 10 tomates y luego otro “sous chef” cuenta los mismos 10 tomates, ambos toman 10 tomates para la salsa, pero ambos informan al Chef que solo quedan 9 tomates en la despensa, lo que es incorrecto.

En resumen, en este ejemplo, el GIL en Python puede causar problemas de concurrencia al permitir que múltiples hilos accedan a recursos compartidos al mismo tiempo, lo que puede llevar a resultados incorrectos o inesperados.

Estos conceptos se traducen en el multiprocesamiento y el GIL en Python:

  1. Multiprocesamiento:
    • El multiprocesamiento sería como tener varias estufas y hornos en tu cocina. Puedes cocinar diferentes platos simultáneamente en cada uno de ellos sin interferir entre sí. Por ejemplo, puedes cocinar la carne en una estufa, las verduras en otra y el postre en el horno, todo al mismo tiempo.
    • Cada estufa u horno representa un proceso independiente en el sistema, y como están separados, no se afectan entre sí. Del mismo modo, cada proceso en Python tiene su propio espacio de memoria y puede ejecutarse de forma independiente de otros procesos.
  2. Global Interpreter Lock (GIL):
    • El GIL sería como tener un solo juego de utensilios de cocina que todos los cocineros tienen que compartir. Solo un cocinero puede usar los utensilios en un momento dado, por lo que aunque tengas múltiples estufas y hornos, solo puedes trabajar en una tarea a la vez.
    • Esto significa que, incluso si tienes múltiples procesos (o cocineros) ejecutándose, solo uno puede ejecutar código Python en un momento dado debido al GIL. Al igual que con los utensilios de cocina, los hilos (o procesos) deben competir por el acceso al GIL para ejecutar su código Python, lo que puede ralentizar el progreso general de las tareas.

En resumen, el multiprocesamiento te permite ejecutar múltiples tareas simultáneamente, cada una en su propio “espacio de cocina”, mientras que el GIL actúa como un conjunto compartido de utensilios de cocina que limita la capacidad de ejecución paralela en Python, aunque tengas múltiples procesos o hilos.

Una función de orden superior de Python es map(), que pertenece al conjunto de funciones de programación funcional. No es específicamente parte de un paquete, sino una función integrada en el lenguaje Python.

La función map() toma una función y un iterable (como una lista) como argumentos y aplica la función a cada elemento del iterable, devolviendo un objeto map que contiene los resultados.

Un ejemplo básico de cómo se usa map():

import time

# Definimos una función que toma un número y devuelve su cuadrado
def cuadrado(x):
    return x ** 2

# Creamos una lista de números
numeros = [1, 2, 3, 4, 5]
#numeros = np.arange(1, 1001)
# Ejemplo con map()
inicio_map = time.time()  # Tiempo de inicio
resultados_map = map(cuadrado, numeros)
print("Resultados con map:", list(resultados_map))  # Imprimimos los resultados
fin_map = time.time()  # Tiempo de finalización
tiempo_map = fin_map - inicio_map  # Calculamos el tiempo transcurrido
print("Tiempo con map:", tiempo_map, "segundos")  # Imprimimos el tiempo transcurrido

# Ejemplo con for
inicio_for = time.time()  # Tiempo de inicio
resultados_for = [cuadrado(x) for x in numeros]
print("Resultados con for:", resultados_for)  # Imprimimos los resultados
fin_for = time.time()  # Tiempo de finalización
tiempo_for = fin_for - inicio_for  # Calculamos el tiempo transcurrido
print("Tiempo con for:", tiempo_for, "segundos")  # Imprimimos el tiempo transcurrido

En este ejemplo, map() aplica la función cuadrado a cada elemento de la lista numeros y devuelve un objeto map que contiene los resultados. Luego, convertimos este objeto map en una lista para imprimir los resultados.

map() es especialmente útil cuando se quiere aplicar una función simple a todos los elementos de una lista o cualquier otro iterable, ya que proporciona una forma más concisa y legible de realizar esta operación en comparación con el uso de un bucle for. El tiempo de ejecución puede variar según el contexto y las condiciones de la ejecución. En teoría, map() podría ser más rápido que un bucle for debido a su optimización interna y su capacidad para realizar operaciones en paralelo. Sin embargo, en la práctica, puede haber varios factores que afecten al rendimiento y que podrían hacer que un enfoque sea más rápido que el otro en determinadas circunstancias.

El tiempo de ejecución puede variar según el contexto y las condiciones de la ejecución. En teoría, map() podría ser más rápido que un bucle for debido a su optimización interna y su capacidad para realizar operaciones en paralelo. Sin embargo, en la práctica, puede haber varios factores que afecten al rendimiento y que podrían hacer que un enfoque sea más rápido que el otro en determinadas circunstancias.

Aquí hay algunos factores a considerar:

  1. Tamaño de los datos: Para conjuntos de datos pequeños, la diferencia de rendimiento entre map() y un bucle for puede ser insignificante y podría depender más del overhead de la función map() y la creación del objeto map.

  2. Naturaleza de la función aplicada: El rendimiento de map() puede verse afectado por la complejidad de la función aplicada. Si la función es muy simple, es posible que el overhead de map() sea mayor en comparación con el bucle for, lo que podría hacer que el bucle for sea más rápido.

  3. Paralelismo: Aunque map() puede realizar operaciones en paralelo internamente, esto puede depender de la implementación subyacente y de la capacidad del sistema para manejar múltiples hilos o procesos. En algunas situaciones, el paralelismo de map() puede no ser aprovechado completamente o puede introducir cierto overhead que ralentiza la ejecución.

  4. Tipo de datos: Algunos tipos de datos pueden ser más eficientemente procesados por map() que por un bucle for. Por ejemplo, map() puede ser más adecuado para operar con listas, mientras que para otros tipos de datos como generadores, el bucle for puede ser más eficiente.

En resumen, aunque map() tiene el potencial de ser más rápido que un bucle for, el rendimiento puede variar según varios factores. Es importante evaluar y comparar el rendimiento de diferentes enfoques en función de las características específicas del problema y del entorno de ejecución.

Ahora, vamos a ver como funciona el paquete multiprocessing.

  1. Importa el módulo multiprocessing, que proporciona funcionalidades para la creación y administración de procesos en paralelo:
import multiprocessing
  1. Esta función devuelve el número de núcleos de la CPU en el sistema actual. Es útil para determinar cuántos procesos se pueden ejecutar simultáneamente en paralelo.
multiprocessing.cpu_count()
  1. Importa la clase Pool del módulo multiprocessing.pool y la renombra como ProcessPool. La clase Pool se utiliza para crear grupos de procesos que pueden ejecutar funciones de manera paralela.
from multiprocessing.pool import Pool as ProcessPool
  1. Define una función llamada sum_numbers que calcula la suma de los números del 0 al n-1 mediante un bucle for.
def sum_numbers(n):
    r = 0
    for i in range(n):
        r += i
    return r
  1. Crea una lista de números que se utilizarán como entrada para la función sum_numbers.
inputs = [60_000_000, 80_000_000, 50_000_000, 70_000_000]
  1. Esta es una “cell magic” de Jupyter Notebook que mide el tiempo de ejecución de la celda de código que sigue. En este caso, se utiliza para medir el tiempo que tarda la ejecución de la celda en calcular la suma de los números utilizando la función map(). Utiliza la función map() para aplicar la función sum_numbers a cada elemento de la lista inputs. Luego, convierte el objeto map resultante en una lista y lo asigna a la variable results.
%%time
results = list(map(sum_numbers, inputs))
results
  1. Crea un contexto de administración de procesos (ProcessPool) con un total de 8 procesos. Esto significa que se crearán hasta 8 procesos que ejecutarán las llamadas a sum_numbers en paralelo. Utiliza el método map() del objeto pool para aplicar la función sum_numbers a cada elemento de la lista inputs en paralelo. El resultado se asigna a la variable result.
%%time
with ProcessPool(processes=8) as pool:  # context manager providing a `Pool` instance
    result = pool.map(sum_numbers, inputs)
print(result)

En resumen, este código calcula la suma de los números del 0 al n-1 para cada valor en la lista inputs, primero utilizando map() para calcularlo secuencialmente y luego utilizando un ProcessPool para calcularlo en paralelo. Luego, muestra los resultados de ambos en el tiempo que tardan en ejecutarse.

Ejercicio: Crea una función para calcular el cuadrado de una lista de números. Ejecutala con %%time para comparar tiempo de ejecución y recursos de las dos maneras, usando paralelización y sin esto.

Veamos un ejemplo más. Este código investiga cómo aumenta la velocidad de ejecución del código al aumentar el número de procesos utilizados para la ejecución en paralelo. Primero crearemos una lista llamada inputs que contiene el valor 10,000,000 repetido 16 veces. Esto significa que la función sum_numbers se aplicará a una lista de 16 elementos, cada uno de longitud 10,000,000. Vamos a inicializar una lista vacía llamada times donde se almacenarán los tiempos de ejecución.

  • n_processes = np.arange(1, multiprocessing.cpu_count() + 4): Crea un array NumPy que contiene números enteros desde 1 hasta el número de núcleos de CPU en el sistema más 3. Esto proporciona un rango de números que representan el número de procesos que se utilizarán para la ejecución en paralelo. Y vamos a iterar sobre cada número en el array n_processes.
import time
import multiprocessing
import numpy as np


inputs = [10_000_000] * 16
times = []
n_processes = np.arange(1, multiprocessing.cpu_count() + 4)
for n in n_processes:
    t0 = time.time()
    with ProcessPool(processes=n) as pool:
        result = pool.map(sum_numbers, inputs)
    times.append(time.time() - t0)

times
import matplotlib.pyplot as plt
import numpy as np

times = np.array(times)
fig, axes = plt.subplots()
axes.plot(n_processes, 1.0 * n_processes, color='k', linestyle='--', label='ideal')
axes.plot(n_processes, times[0] / times, marker='o', label='measured')
axes.set_xlabel(r'$n$ processes')
axes.set_ylabel('relative speedup')
fig.legend()

Links útiles: