Lectura - Estructuras de Datos
Lectura - Estructuras de Datos
Estructuras de Datos
Ciclos for
for i in range(10):
print(i)
Para crear una función ocuparemos la instrucción def
def funcion():
def foo():
return bar = 5
foo()
print(bar)
El alcance de una variable significa desde donde podemos accederla. Hemos estudiado dos tipos de
variables
Las variables globales son peligrosas, especialmente cuando trabajamos con mas personas o código
de otros puesto que es muy probable que sobreescribamos variables.
Glosario
Lista (Arreglo o Array): Estructura de datos que nos permite guardar varios datos en una variable.
Corchetes: [] Utilizados para la creación de listas.
Estructura de datos: Objeto que permite almacenar y organizar los datos para que puedan ser
utilizados de forma eficiente.
Índice: Posición que ocupa un elemento en una lista. En una lista, los índices comienzan en 0.
Iterar: Recorrer los elementos de una lista.
Filtrar: Seleccionar o mostrar dentro de una lista solo los elementos que cumplen con alguna
condición.
Reducir: Devuelve un único valor con el resultado de una operación aplicada a la lista.
Bloque: También se llama así al vector o conjunto de datos de una lista.
Persistencia: Grabar datos modificados en una estructura.
Mutabilidad: Capacidad que tiene una lista de permitir la modificaciónn de sus elementos.
Ingesta de datos: Proceso de introducción de datos al sistema que se construye.
Capítulo 1: Introducción a listas (arrays)
Objetivos:
Conocer el uso de las listas
Crear listas
Mostrar una lista
Acceder a un elemento de un lista ocupando su índice
Introducción a Listas
Las listas, también conocidas como arreglos en español, son contenedores que permiten agregar múltiples
datos; En lugar de guardar un sólo número o un solo string, nos permitirán guardar varios
simultaneamente.
# Estos elementos pueden ser de un mismo tipo de datos, por ejemplo solo strings:
animales = ['gato','perro','raton']
Los elementos de la lista se pueden modificar, ya sea modificando los valores de éstos, agregando
elementos, o eliminando elementos. Por eso es que se dice que las listas son mutables.
Las listas son muy utilizadas dentro de la programación. Son útiles para resolver diversos tipos de
problemas. Por ejemplo, si se quiere hacer una aplicación web, se puede traer los datos de la base de datos
a una lista y operar los datos desde ahí. También son útiles para leer arhcivos y crear gráficos entre
múltiples otras funciones.
a = [1, 2, 3, 4]
Se definen con los paréntesis de corchete [] ; Todo lo que esté dentro de los [] , separados por coma,
son los elementos de la lista.
Lo más sencillo que podemos hacer con una lista es mostrar sus elementos. Para esto podemos ocupar
print o llamar a su objeto contenedor.
[1, 2, 'hola', 4]
[1, 2, 3, 4]
Índices
Cada elemento en la lista tienen una posición específica, la que se conoce como índice.
El índice permite acceder al elemento que está dentro de la lista. Los índices van de cero hasta n - 1 ,
donde n es la cantidad de elementos en la lista.
En una lista que contiene 4 elementos, el primer elemento está en la posición cero, y el último en la 3.
Para acceder al elemento de una posición específica de la lista, simplemente se debe escribir el nombre de
la lista, y entre paréntesis de corchetes, el índice de la posición.
verde
rojo
azul
colores[8]
---------------------------------------------------------------------------
<ipython-input-4-cf0cdf800f9c> in <module>()
----> 1 colores[8]
Índices negativos
Los índices también se pueden utilizar con números negativos y de esta forma referirse a los elementos
desde el último al primero.
a = [1, 2, 3, 4, 5]
a[-1]
Desafío
En base a la lista a , ¿qué se muestra en cada petición con los índices?
a = [1, 2, 3, 4, 'hola', 8]
a[0]
a[7]
a[a[0]]
a[4]
a[-1]
ARGV
import sys
type(sys.argv)
list
Resumen:
Las listas nos permiten guardar varios datos en una sola variable.
Podemos mostrar los datos dentro de una lista con print o llamándolas desde su objeto contenedor.
Los elementos dentro de una lista tienen un índice que indica su posición.
Podemos acceder al elemento ocupando el índice, ej: a[0] => primer elemento
Los índices negativos nos permiten acceder a la lista desde atrás, ej: a[-1] => Último elemento
Utilizar un índice más allá del largo de una lista arroja un error IndexError .
Capítulo 2: Operaciones y funciones básicas
en una lista
Objetivos:
Leer documentación del objeto list .
Agregar elementos a una lista.
Borrar elementos a una lista.
Contar elementos en una lista.
Saber si un elemento se encuentra dentro de una lista.
Motivación
Las listas de Python tienen una serie de funciones que permiten realizar operaciones básicas, entre ellas,
ordenar los elementos de una lista, saber cuántas veces se encuentra un elemento en la lista, borrar y
agregar elementos. Por lo mismo, es muy importante saber leer la documentación asociada a éstas.
Cuando generamos una nueva lista y la asignamos a una variable, su generación viene con una serie de
atributos y funciones asociadas. Esta es otra de las virtudes del paradigma objeto orientado: cada objeto o
representación creada vendrá con una serie de funcionalidades agregadas.
La forma canónica para llamar a una función de un objeto se llama notación de punto:
objeto.función(argumentos) .
Al generar un objeto llamado lista_de_numeros que se compone de los números del 1 al 10, Python le
concederá una serie de acciones dado que infiere que su mejor representación es una lista. Se puede ver
cuáles son todas las posibles acciones, utilizando el atributo __dir__() .
print(lista_de_numeros.__dir__())
['__repr__', '__hash__', '__getattribute__', '__lt__', '__le__', '__eq__', '__ne__',
'__gt__', '__ge__', '__iter__', '__init__', '__len__', '__getitem__', '__setitem__',
'__delitem__', '__add__', '__mul__', '__rmul__', '__contains__', '__iadd__', '__imul__',
'__new__', '__reversed__', '__sizeof__', 'clear', 'copy', 'append', 'insert', 'extend',
'pop', 'remove', 'index', 'count', 'reverse', 'sort', '__doc__', '__str__',
'__setattr__', '__delattr__', '__reduce_ex__', '__reduce__', '__subclasshook__',
'__init_subclass__', '__format__', '__dir__', '__class__']
Aquellos elementos que están envueltos por __ se conocen como magic built-in o dunders, y buscan
generar flexibilizaciones en el comportamiento de la clases. Serán retomados cuando hablemos de clases.
Todas las que no son dunders son las acciones disponibles a acceder.
Función append(x)
Para agregar elementos a una lista, se debe utilizar la función append . Por ejemplo, si queremos agregar el
celeste a nuestra lista de colores, se hace de la siguiente forma:
colores.append("celeste")
Cabe destacar que no es necesario declarar de forma explícita la asignación al objeto, dado que append es
una función inherente al objeto, opera y lo actualiza dentro de ella.
Función insert(i, x)
Esta función nos permite agregar elementos en una posición específica.
Para resolver este problema, podemos especificar en qué posición de la lista vamos a incorporar un
elemento. Para poder ingresar un elemento en una posición específica de la lista podemos hacer uso de la
función .insert(i, x) . La función necesita la posición a modificar como primer argumento (i) y el
elemento a ingresar como el segundo (x) . Insertemos el número 12 donde corresponde.
lista_de_numeros.insert(11, 12)
lista_de_numeros
Función pop()
Para sacar el último elemento dentro de una lista, y obtenerlo, se debe utilizar la función .pop() . Por
defecto, la función eliminará el último elemento de la lista y lo imprimirá. Si la expresión es asignada a una
variable, la variable almacenará el elemento que se sacó.
También es posible pasar como argumento de la función una posición específica. De esta forma, se buscará
dentro de la lista el elemento en esa posición, y éste será eliminado y entregado.
colores.pop()
'celeste'
colores.pop(3)
'azul'
Función remove(x)
Para remover un elemento específico, se utiliza la función remove(x) , donde x es el elemento específico
a eliminar. En caso que el elemento no esté presente en la lista, Python arrojará un error ValueError .
colores.remove("verde")
colores
['rojo', 'rosa']
colores.remove("azul")
---------------------------------------------------------------------------
<ipython-input-13-86badd94da6c> in <module>()
----> 1 colores.remove("azul")
Función reverse()
Se puede invertir el orden de los elementos de una lista utilizando .reverse .
numeros
animales
Función sort()
Se puede ordenar los elementos de forma ascendente utilizando .sort . En caso de que se trate de strings,
se ordenan de forma ascendente en el abecedario
animales.sort()
numeros.sort()
animales
numeros
Si se quiere ordenar de forma descendente, se puede aplicar reverse() luego de aplicar sort() .
numeros.reverse()
numeros
Un aspecto relevante a recalcar y tomar en cuenta, es que al trabajar con listas en Python todas estas
funciones se realizaron sobre la misma lista. Por tanto, si no se guarda la lista original en algún objeto
separado, esta información se pierde.
Aplicaremos un menú de opciones, y un ciclo while para integrar también contenido de unidades
anteriores.
if cambiar == 'S':
ingredientes.remove("masa delgada")
ingredientes.insert(0, "masa tradicional")
else:
print("¡Su pizza no tiene masa!")
print("Escoja '1' si desea masa tradicional, o '2' si desea masa delgada")
print()
masa = input()
if masa == "1":
ingredientes.insert(0, "masa tradicional")
elif masa == "2":
ingredientes.insert(0, "masa delgada")
nuevo = input.lower()
nuevo = input().lower()
elif opcion == 5:
# Consultamos el ingrediente para eliminar
print("Ingredientes actuales:", ingredientes)
print("Escriba el que desea eliminar")
print()
eliminar = input().lower()
Sugerencias de mejoras
1. Validaciones para que dentro de las opciones principales solo se escojan de la 1 a la 6
2. Validaciones para que dentro de las opciones que lo requieran, solo se ingresen los campos
permitidos.
3. Agregar más mensajes de print para ayudar en el flujo al usuario.
4. Replicar lógica de las masas con la salsa (salsa de tomate o salsa BBQ).
5. Crear otra lista con los “ingredientes base”, y utilizar esta lista para mostrar los ingredientes
disponibles, y validar la entrada del usuario.
6. Consultar por agregar o eliminar ingredientes continuamente (sin tener que volver al menú principal)
hasta que el usuario indique que ya no desea hacer modificaciones.
7. Contar la frecuencia de cada ingrediente.
8. Ordenar los ingredientes alfabéticamente, omitiendo la masa y la salsa que deben salir al comienzo
9. Omitir masa y salsa en la lista de ingredientes para eliminar
10. Asignar un precio a cada ingrediente y calcular el valor final de la pizza, y mostrarlo al escoger la opción
“ordenar”
Un aspecto a mencionar es que muchas de las operaciones también son inferidas en función de las
estructuras de datos a manipular. Si sabemos que podemos concatenar dos strings con + , cuando
concatenamos dos listas obtenemos lo siguiente:
La fución len() responde de similar manera a los strings cuando es aplicado en una lista. Genera un
reporte de la cantidad de elementos contenidos en la lista.
Lo que vamos a realizar es multiplicar los elementos contenidos en la lista mediante el operador * .
animales_actualizados = animales *4
Membresías en listas
Se puede saber si cierto elemento se encuentra en la lista, mediante el operador booleano in . Por
ejemplo, queremos ver si encontramos un Tapir en la lista de mascotas
'Tapir' in mascotas
False
Por ser un operador booleano, el retorno del operador in solo puede ser True o False . Si ahora
preguntamos por la existencia de gatos, podríamos hacer lo siguiente:
'Gato' in mascotas
True
También es posible saber cuántos elementos existen de un tipo, mediante la función .count() que existe
en la lista. No confundir con la función count() de los objetos string.
mascotas.count('Gato')
Comprensiones de listas
Un elemento en particular de Python es la existencia de comprensiones de listas. Mediante éstas, es posible
modificar todos sus elementos y retornar una nueva lista con los nuevos elementos, de forma más concisa
que utilizando el tradicional ciclo for .
Ejemplo
Se requiere escribir en mayúsculas todos los nombres de animales en nuestra lista mascotas . Sin
comprensiones de lista, esto se puede lograr como:
mascotas_mayusculas = []
for i in mascotas:
mascotas_mayusculas.append(i.upper())
mascotas_mayusculas
['GATO',
'PERRO',
'TORTUGA',
'GATO',
'PERRO',
'TORTUGA',
'GATO',
'PERRO',
'TORTUGA',
'GATO',
'PERRO',
'TORTUGA',
'HURÓN',
'HAMSTER',
'ERIZO DE TIERRA']
Si bien el ejercicio es válido, se pierde tiempo en definir la lista contenedora y realizando append en ésta
en cada iteración. Una comprensión de lista presenta una forma más elegante de realizar lo mismo.
['GATO',
'PERRO',
'TORTUGA',
'GATO',
'PERRO',
'TORTUGA',
'GATO',
'PERRO',
'TORTUGA',
'GATO',
'PERRO',
'TORTUGA',
'HURÓN',
'HAMSTER',
'ERIZO DE TIERRA']
¿Qué pasa si se desea poner en mayúsculas solo a los gatos, y los demás en minúsculas?. La solución con
for sería la siguiente:
mascotas_mayusculas = []
for i in mascotas:
if i == 'Gato':
mascotas_mayusculas.append(i.upper())
else:
mascotas_mayusculas.append(i.lower())
mascotas_mayusculas
['GATO',
'perro',
'tortuga',
'GATO',
'perro',
'tortuga',
'GATO',
'perro',
'tortuga',
'GATO',
'perro',
'tortuga',
'hurón',
'hamster',
'erizo de tierra']
Mientras que con una comprensión de lista:
['GATO',
'perro',
'tortuga',
'GATO',
'perro',
'tortuga',
'GATO',
'perro',
'tortuga',
'GATO',
'perro',
'tortuga',
'hurón',
'hamster',
'erizo de tierra']
Capítulo 3: Iterando una lista a partir del
índice
Objetivos:
Recorrer los elementos de una lista.
Entender la importancia de guardar la lista original en ciertos casos.
Transformar los elementos dentro de la lista mientras es recorrida.
Motivación
En este capítulo estudiaremos cómo recorrer una lista y operar sobre sus datos para resolver diversos tipos
de problema.
Formas de iterar
En Python existen diversas formas de iterar una lista o un arreglo. Cada una tiene sus puntos a favor y en
contra. A grandes rasgos, podemos identificar las siguientes estrategias de iteración:
Iterando una lista por posición
Hemos visto que es posible acceder a un elemento de una lista en una posición específica utilizando la
sintaxis a[posición] . Esto se puede aplicar al iterar sobre ciclos for o while .
Con for
a = [1, 2, 3, 4, 5, 6]
n = len(a)
for i in range(n):
print(a[i])
1
2
3
4
5
6
Con while
count = 0
1
2
3
4
5
6
n = 5
i = 0
while i < n:
print(a[i])
i += 1
1
2
3
4
5
El problema existente en este caso es que toda la nueva información agregada en la lista (números 10, 13 y
42), es ignorada. Para evitar éste tipo de problema, es mejor implementar la condición de entrada como n
= len(a) . Esto hace que el ciclo y el código sea dinámico a nuevas versiones de la lista n .
Iterando la lista por sus elementos
Al iterar directamente una lista en un ciclo for , el iterador i toma el valor del elemento recorrido en
cada iteración, no su índice.
Ejemplo
Se tiene una lista heterogénea (que tiene elementos con distintos tipos de dato), y se desea contar los
elementos strings accediendo a su posición específica.
count_str = 0
for i in lista_heterogenea:
if type(lista_heterogenea[i]) is 'str':
count_str += 1
else:
pass
---------------------------------------------------------------------------
<ipython-input-25-f67ebd4371c7> in <module>()
5 count_str = 0
6 for i in lista_heterogenea:
----> 7 if type(lista_heterogenea[i]) is 'str':
8 count_str += 1
9 else:
En este ejemplo, Python arrojará un TypeError: list indices must be integers or slices, not str .
Este error indica que el índice de la lista es inválido.
Esto se debe a que se está utilizando un string, el elemento que se está recorriendo en la iteración, como
índice de la posición del elemento.
Para resolver este problema, se necesita encontrar una función que entregue tanto la posición como el
elemento en sí de la lista. Para ello se debe utilizar enumerate .
Primero veremos qué entrega la función.
Mediante enumerate se puede acceder tanto al elemento mismo como a su índice dentro de la lista. Un
aspecto a considerar es que se debe declarar los dos iterables en el ciclo for . Si no se quiere extrar el
elemento y solo se requiere el índice, se puede ignorar su iterable en las expresiones, o implementar un
placeholder representado con _
0
1
2
3
4
5
6
7
8
9
10
11
12
Ya entendiendo qué hace enumerate , es fácil desarrollar la solución:
count_str = 0
for i, _ in enumerate(lista_heterogenea):
if type(lista_heterogenea[i]) is str :
count_str += 1
else:
pass
Transformar el tipo de dato de todos los elementos, por ejemplo de entero a string.
Filtrar (seleccionar o mostrar) solo los elementos que cumplen algún criterio.
Reducir; Por ejemplo sumar todos los elementos, concatenarlos o multiplicarlos.
Transformación
Sabiendo que el usuario puede introducir múltiples datos vía sys.argv , ¿Qué pasaría si se quiere
transformar todos estos datos a enteros?. Para lograrlo, se debe que recorrer el arreglo de sys.argv
elemento a elemento, transformar cada uno, y guardando cada nuevo elemento en una nueva lista.
import sys
user_arguments = sys.argv
n = len(user_arguments)
new_arguments = []
for i in range(n):
if i > 0:
new_arguments.append(int(user_arguments[i]))
Filtrado
Filtrar una lista consiste en seleccionar elementos específicos omitiendo el resto mediante una condición.
Por ejemplo, podríamos crear un programa que maneje datos astronómicos, y se solicite descartar (filtrar)
las mediciones erróneas.
Ejemplo de filtrado
Se pide crear un programa que filtre todos los números de una lista que sean menores a 1000. Esto es lo
mismo que decir "seleccionar todos los elementos mayor o iguales a mil".
for i in range(n):
if a[i] >= 1000:
filtered_array.append(a[i])
filtered_array
Resumen
En este capítulo se estudió la base de resolución de problemas con listas. A partir de ahora se seguirá
trabajando con esta lógica para resolver diversos tipos de problemas.
Objetivos:
Aplicar operaciones de listas.
# Solución
def adictos_a_redes(lista):
results = []
for i in lista:
if i > 90:
results.append('mal')
else:
results.append('bien')
return results
Desafío - Adictos v2
Se pide crear el programa adictos2.0.py con una función llamada scan_addicts2 .
La función debe recibir una lista con los minutos de uso de los usuarios.
Debe retornar una nueva lista cambiando todas las medidas inferiores a 90 minutos como 'bien', entre
90 y 180 como 'mejorable', y todas las mayores o iguales a 180 como 'mal'.
Tip: Cuidado con las condiciones de borde; analizar los casos de 90 y 180
# Solución
def scan_addicts2(lista):
results = []
for i in lista:
if i >= 180:
results.append('mal')
elif i >= 90:
results.append('mejorable')
else:
results.append('bien')
return results
lista_adictos = [120, 90, 600, 30, 90, 10, 200, 180, 500]
print(scan_addicts2(lista_adictos))
tip_: Por defecto Python 3 implementa una división que devuelve un flotante. Lo que necesitamos es la
parte entera. Ocupe // para resolver este inconveniente.
# Solución
def to_minutes(lista):
results = []
for i in lista:
results.append(i // 60)
return results
Desafío: normalización.py
La normalización vectorial busca reexpresar entre 0 y 1 una serie de valores contenidos en una lista. Suena
mucho más complejo de lo que es.
Consiste en que, dado un arreglo con valores, se genere un nuevo arreglo donde todos los valores están
entre cero y uno.
Para lograrlo, cada valor del arreglo se debe dividir por el módulo, y el módulo se calcula como la raíz de la
suma de cada uno de los elementos al cuadrado.
Se necesita:
El resultado es posible de verificar dado que la suma de todos los valores al cuadrado debe ser uno.
Tip:_ Para implementar la raíz cuadrada, importar el módulo math y ocupar la función sqrt
# Solución
import math
def modulo(lista):
suma = 0
for i in lista:
suma = suma + i**2
return math.sqrt(suma)
vector = [1, 2, 3, 4, 5, 6]
modulo(vector)
3.7416573867739413
def normalizar(lista):
m = modulo(lista)
normalized_array = []
for i in lista:
normalized_array.append(i / m)
return normalized_array
## Verificamos el resultado
# Dijimos que la suma de cada uno de los números al cuadrado debe ser uno,
# esto lo podemos probar iterando el arreglo.
suma = 0
for i in output_array:
suma += i ** 2
print(suma)
Desafío: Artículos de tienda
Se tiene una lista de categoría, nombres y precios de artículos que están intercalados, donde primero
viene la categoría, luego el nombre del artículo y luego su precio.
Los precios están ingresados como datos de texto, no como numéricos:
articulos = ["celular", "LG K10", "90000", "tablet", "Galaxy TAB", "80000", "smart tv",
"LED 43 Samsung", "485000", "celular", "Galaxy J7", "120000", "celular", "Huawei Y5",
"59900", "notebook", "Lenovo ideapad", "250000", "tablet", "Huawei media", "139000",
"notebook", "Acer", "145000"]
El dueño de la tienda requiere detectar aquellos artículos que cuesten más de $80.000, que no sean
notebooks, y aplicarles un descuento de 10%.
Se requiere que estos nuevos precios se separen en listas por categoría de artículo, ordenados de
mayor a menor.
Solución
articulos = ["celular", "LG K10", "90000", "tablet", "Galaxy TAB", "80000", "smart tv",
"LED 43 Samsung", "485000",
"celular", "Galaxy J7", "120000", "celular", "Huawei Y5", "59900",
"notebook", "Lenovo ideapad", "250000",
"tablet", "Huawei media", "139000", "notebook", "Acer", "145000"]
notebooks.sort()
notebooks.reverse()
smart_tv.sort()
smart_tv.reverse()
tablets.sort()
tablets.reverse()
# Output
print("Celulares:", celulares)
print("Notebooks:", notebooks)
print("Smart tv:", smart_tv)
print("Tablets:", tablets)
Capítulo 5: Operaciones funcionales sobre
listas
Objetivos:
Conocer las operaciones funcionales y sus ventajas.
Aprender a utilizar map .
Aprender a utilizar reduce .
Aprender a utilizar filter .
Map
Map sirve para aplicar una operación a cada elemento de la lista. Al aplicarse, devuelve un objeto de tipo
map que puede ser convertido a una lista
La sintáxis de map toma dos parámetros de entrada: la expresión a ejecutar y en qué lista se va a aplicar la
función.
lambda x: x * 2
donde:
lambda es la expresión que informa a Python que vamos a desarrollar una función anónima.
x es el parámetro a modificar.
: es el punto de cierre. Posterior a éste, todo se considerará como expresiones a ejecutar.
# con for
a = [1, 2, 3, 4, 5, 6, 7]
b_for = []
for i in a:
b_for.append(i * 2)
b_for
# con map
b_map
b_for == b_map
True
Filter
La segunda operación funcional muy útil sobre listas es filter . Ésta permite extraer todos los elementos
que cumplan con algún criterio específico. De similar manera que con map , nos puede retornar una lista si
lo envolvemos con el método list .
a = [1,2,3,4,5,6,7]
return_even = []
for i in a:
if i % 2 == 0:
return_even.append(i)
return_even_for
[2, 4, 6]
a = [1,2,3,4,5,6,7]
return_even_filter
[2, 4, 6]
return_even_for == return_even_filter
True
Reduce
Reduce permite operar sobre todos los resultados de la lista. En lugar de devolver una lista, devuelve un
único valor con el resultado de la operación aplicada. Por ello, el bloque requiere que se pasen dos
variables; la que itera y la que guarda el resultado.
suma = 0
for i in a:
suma += i
print(suma)
28
Capítulo 6: Pandas
Objetivos:
Importar la librería pandas para la ingesta y el preprocesamiento de datos numéricos.
Conocer las principales estructuras de datos de pandas y sus dimensiones.
Manipular un archivo .csv desde pandas .
Realizar operaciones de selección en un DataFrame .
Conocer las principales operaciones en una Serie .
Generar iteraciones con los distintos métodos de DataFrame .
Introducción a Pandas
Muchas veces trabajaremos con datos que vienen ordenados en archivos de texto plano como .csv ,
.xls , .txt . Si bien es posible realizar ingesta de éstos con Python nativo, haremos uso explícito de
librerías específicas para esta tarea.
En este capítulo se utilizará pandas , una librería orientada a la manipulación y limpieza de estructuras de
datos que provee estructuras de datos y funciones orientadas a hacer el trabajo con tablas más rápido, fácil
y expresivo (McKinney, 2014, 4).
Ésta librería combina el alto desempeño de operaciones vectorizadas de numpy (a revisar en el Capítulo 7)
con las manipulaciones flexibles de hojas de cálculo y bases de datos relacionales.
Existen dos grandes estructuras de datos en pandas : los DataFrame y las Series . Cada una tiene
peculiaridades que revisaremos a detalle posteriormente.
Para poder trabajar con pandas , debe importarse con la siguiente nomenclatura:
import pandas as pd
De esta manera, cada vez que se quiera utilizar alguna función de pandas , se debe preceder con el alias
pd .
Leyendo archivos csv
Para ingresar una base de datos en formato csv , se utiliza la función pd.read_csv . Este método
devolverá la representación pandas del archivo en el espacio de trabajo. Una de las buenas prácticas
indica guardar la base de datos en la memoria del espacio de trabajo de la siguiente manera:
df = pd.read_csv('nations.csv')
DataFrame
Un DataFrame es la representación de toda la información existente del archivo. De esta forma, al
importar la base de datos al ambiente de trabajo, parte del procesamiento interno de los datos realizados
en pandas es asignarle una clase específica, llamada DataFrame .
Esta clase concederá una serie de atributos, funciones y opciones al objeto para poder manipularlo. Por
ejemplo, inspeccionar los datos ingresados.
Para averiguar las dimensiones de los datos (cuántas filas y cuántas columnas tiene la base), se debe
acceder a la propiedad df.shape . El retorno del comando sugiere que existen 194 filas y 14 columnas.
df.shape
(194, 14)
También es posible ver los datos de las primeras filas, utilizando la función df.head() . Por defecto, la
función retornará las 5 primeras observaciones. Si se desea solicitar más o menos, se debe ingresar la
cantidad de observaciones entre sus paréntesis, como argumento.
df.head(3)
df = df.drop(columns='Unnamed: 0')
(3, 14)
Series
Hasta el momento se han aplicado operaciones que afectan a la tabla en su conjunto. Pero en la práctica,
muchas de las operaciones de interés se realizan a nivel de columna.
En pandas , las columnas se conocen como Series . A grandes rasgos, las Series no distan de las listas
en sus principales componentes: Ambas son vectores unidimesionales que almacenan una cantidad finita
de datos.
La ventaja de las Series es que presentan una serie de acciones y atributos que permiten extraer
información de manera rápida.
Por ejemplo, supongamos que queremos contar cuántas observaciones pertenencen a cada país. Para
lograr esto, partamos por acceder a la columna específica:
df['region']
0 Africa
1 Africa
2 Africa
3 Africa
4 Africa
5 Africa
6 Africa
7 Africa
8 Africa
9 Africa
10 Africa
11 Africa
12 Africa
13 Africa
14 Africa
15 Africa
16 Africa
17 Africa
18 Africa
19 Africa
20 Africa
21 Africa
22 Africa
23 Africa
24 Africa
25 Africa
26 Africa
27 Africa
28 Africa
29 Africa
...
164 Europe
165 Europe
166 Europe
167 Europe
168 Europe
169 Europe
170 Europe
171 Europe
172 Europe
173 Europe
174 Europe
175 Europe
176 Europe
177 Europe
178 Europe
179 Oceania
180 Oceania
181 Oceania
182 Oceania
183 Oceania
184 Oceania
185 Oceania
186 Oceania
187 Oceania
188 Oceania
189 Oceania
190 Oceania
191 Oceania
192 Oceania
193 Oceania
Name: region, Length: 194, dtype: object
Dentro de la serie, es posible conocer la cantidad de cuántas veces se presenta cada atributo dentro de ella,
utilizando la función value_counts() .
df['region'].value_counts()
Africa 52
Asia 49
Europe 43
Americas 35
Oceania 15
Name: region, dtype: int64
Acorde a los resultados, observamos que la mayor cantidad de países pertenencen a África. ¿Qué pasa si se
quiere reportar las probabilidades? Una posibilidad es ocupar el operador / para dividir cada registro por
la cantidad de países existentes. Esto se conoce como una operación vectorizada, y se estudiará con
detención más adelante
df['region'].value_counts() / len(df)
Africa 0.268041
Asia 0.252577
Europe 0.221649
Americas 0.180412
Oceania 0.077320
Name: region, dtype: float64
Al trabajar con una Serie, el retorno de una operación será otra Serie. Por tanto, es posible concatenar
acciones a éstas. En este caso, si se desea identificar el promedio de países por continente, es posible
hacerlo concatenando la función mean al final de value_counts .
df['region'].value_counts().mean()
38.8
Para acceder a alguna observación específica (ya sea fila o columna), se dene hacer mediante a notación de
loc e iloc :
loc: Localiza una observación específica mediante el nombre de una columna y su posición. La forma
para acceder a una observación responde a la siguiente nomenclatura
Por ejemplo, para acceder al campo 'life' de la novena observación, la sintaxis es:
df.loc[8, 'life']
48.5666656494141
También es posible acceder a la fila completa, incuyendo todas las columnas. Esto se logra mediante el
operador : (slice). Éste indica que se está considerando todos los elementos de una lista o serie (los
nombres de las columnas también son una Serie).
En este caso, le instruímos a pandas extraer todos los registros de los campos en la novena
observación.
df.loc[8,:]
country Chad
region Africa
gdp 1266.2
school 1.5
adfert 164.5
chldmort 209
life 48.5667
pop 10509983
urban 26.4
femlab 0.8006
literacy 33.6
co2 0.1
gini NaN
Name: 8, dtype: object
También es posible hacer un subset de datos seleccionando todas las filas y un rango específico de
columnas.
Queremos extraer sólo las columnas gdp school adfert chldmort de este subset
En caso de que se requiera seleccionar "desde" una columna "hasta" otra columna (incluyendo todas las
columnas intermedia)s, también es posible el uso de slice : . Como slice va a seleccionar todo, sólo sirve si
el rango de columnas que se desea es contínuo.
df_col_subset == df_col_subset_1
Crear un subsert mediante filtrado
Otra de las fortalezas de pandas , es la facilidad con la cual se puede realizar filtrado de datos. Por ejemplo,
si se desea hacer un subset solo con los países de origen americano, esto se logra con la siguiente sintáxis:
Lo que se hace mediante esta sintaxis, es consultar a pandas por todas aquellas observaciones que
contienen 'Americas' en su campo 'region' .
Como es una operación lógica, ésta entregará una lista de valores True o False . A partir de este punto, el
DataFrame interpreta que seleccionará todos aquellos registros donde la condición evaluada sea
verdadera.
Este nuevo objeto obtenido con lo que se ha seleccionado, seguirá siendo un DataFrame , y por ende tiene
todos sus atributos:
# tamaño
df_americas.shape
(35, 13)
42895.0
84 United States
Name: country, dtype: object
Otro ejemplo:
Se desea extraer solo la información de la columna gdp , que representa el Producto Interno Bruto:
df['gdp']
0 7300.399902
1 1338.800049
2 12307.400391
3 1063.400024
4 349.200012
5 1986.800049
6 3052.199951
7 677.000000
8 1266.199951
9 1099.000000
10 3628.000000
11 279.799988
12 1539.199951
13 1972.199951
14 4754.399902
15 27645.800781
16 579.599976
17 741.400024
18 13183.200195
19 1218.400024
20 1303.000000
21 957.599976
22 967.599976
23 1405.400024
24 1284.400024
25 345.000000
26 14497.799805
27 919.799988
28 660.400024
29 1032.800049
...
164 36646.398438
165 48169.398438
166 15446.400391
167 21628.400391
168 10560.400391
169 13424.799805
170 NaN
171 9473.200195
172 18552.199219
173 25321.199219
174 27862.199219
175 33621.398438
176 37105.800781
177 6124.000000
178 33295.800781
179 4662.000000
180 33707.199219
181 4259.600098
182 2294.199951
183 NaN
184 2906.800049
185 NaN
186 25199.599609
187 NaN
188 1953.800049
189 4012.600098
190 2249.199951
191 4072.199951
192 NaN
193 3809.800049
Name: gdp, Length: 194, dtype: float64
# Para esto concatenaremos funciones. Partimos por preguntar si los valores son
# nulos o no, esto devolverá un booleano. Posteriormente con sum podemos
# contar la cantidad de ocurrencias
df['gdp'].isnull().sum()
15
Observamos que existen 15 observaciones nulas, porque la función "sum" considerará cada evaluación
True como 1 y cada evaluación False como 0.
Para lograr esto, es posible ingresar una operación lógica entre los corchetes. En este caso, la operación
lógica es de la siguiente forma df['gdp'].isnull() == True .
Formas de iteración en un DataFrame
Hasta el momento se ha visto cómo generar "subsets" de datos seleccionando tanto filas como columnas
específicas. Pero algo que puede sea necesario para el flujo de trabajo, es iterar fila a fila y evaluar cada una
para algún caso específico.
for i in df:
print(i)
country
region
gdp
school
adfert
chldmort
life
pop
urban
femlab
literacy
co2
gini
Esto no es satisfactorio, dado que queremos extraer las observaciones pero en su lugar se obtiene los
nombres de las columnas.
En el caso de un DataFrame , existe más de una forma de poder recorrer los registros. Para implementar
estos flujos, están los métodos df.iterrows() , df.iteritems() y df.itertuples() :
df.iteritems()
Con iteritems , es posible recorrer todos los elementos de un DataFrame por columna. Esto significa
que se va a estar recorriendo todos los campos de una base de datos a la vez.
country
region
gdp
school
adfert
chldmort
life
pop
urban
femlab
literacy
co2
gini
Es importante recalcar que iteritems , de similar manera a cuando se utilizaba enumerate , necesita
tener dos iterables declarados. El primer iterable responde al nombre de la columna, mientras que el
segundo devuelve los valores asociados a cada columna en forma de Serie . Este punto es importante,
dado que es posible concatenar acciones que operen a nivel de serie dentro del loop.
Por ejemplo, inspeccionemos si todos los datos corresponden al tipo numérico o no. Para ello, se hará uso
de la siguiente función de pandas : pd.api.types.is_numeric_dtype .
df.iterrrows()
De similar manera que con iteritems , con iterrows es posible recorrer todos los elementos de un
DataFrame por fila. Esto significa que se va a estar recorriendo todas las columnas, por registro, de una
base de datos a la vez.
Por motivos prácticos, para ver el efecto de iterrows , vamos a seleccionar sólo una observación,
incluyendo un if .
country Benin
region Africa
gdp 1338.8
school 3.1
adfert 111.7
chldmort 122.75
life 54.7333
pop 8237634
urban 41
femlab 0.8482
literacy 41.7
co2 1.2
gini NaN
Name: 1, dtype: object
Observamos que lo que devuelve es el registro específico de Benin. Esto es análogo a consultar lo siguiente:
df[df['country'] == 'Benin']
Capítulo 7: Numpy
Objetivos
Importar la librería numpy para el procesamiento y cálculo de datos numéricos.
Manipular los principales tipos de datos de numpy .
Conocer e implementar las principales funciones vectorizadas de numpy .
Integrar funciones de numpy a objetos pandas .
¿Qué es Numpy?
Numpy es un módulo o librería, que constituye una de las piedras angulares en el desarrollo de la
computación numérica en Python. La mayoría de los módulos implementados hacen uso de los objetos de
numpy.
Para enteder la velocidad de numpy, mediremos el tiempo que se demora en ejecutar un loop entre una
lista y un ndarray , que es su equivalente en numpy de una lista.
import numpy as np
%time
demo_list_2 = [x * 2 for x in demo_list]
Ejemplo
Generemos una matriz de de números aleatorios. Para ello utilizamos np.random.randn()
datos_aleatorios = np.random.randn(4,4)
datos_aleatorios
ndarray permite ejecutar operaciones aritméticas en bloques completos de datos de manera similar a
como lo realizamos con escalares. Ahora vamos a multiplicar todos los elementos dentro de la matriz recién
creada:
datos_multiplicados = datos_aleatorios * 3
datos_multiplicados
array([[-1.77962623, -0.86044967, 1.32040443, -2.62188078],
[-1.27020811, -0.42860108, -1.63326795, -0.5734306 ],
[ 2.87782355, -1.74786086, -1.34693736, 5.1939337 ],
[ 2.65216977, 3.17790023, -0.20790038, -2.97077868]])
Ahora podemos comparar si las operaciones hacen sentido al evaluar la división de nuestros
datos_multiplicados en 3, comparados a los datos aleatorios.
datos_aleatorios == (datos_multiplicados / 3)
Los ndarray son contenedores multidimensionales genéricos para datos homogéneos. Esto es muy
relevante, porque para aprovechar la naturaleza del tipeo dinámico de Python, debemos asegurarnos de la
homogeneidad de los elementos a ingresar. Mediante esta definición de datos homogéneos, es posible
realizar esta serie de operaciones de manera fácil y rápida.
Todo ndarray tiene un shape asociado que se expresa mediante una tupla. La tupla nos entrega
información sobre las dimensiones y tamaño de cada dimensión. También tiene un dtype qure nos
entrega información sobre los tipos de datos contenidos en el ndarray .
print(datos_aleatorios.shape)
print(datos_aleatorios.dtype)
(4, 4)
float64
Ejemplos de crear ndarrays
La manera más fácil es implementar los objetos en formato secuencial como las listas, dentro de la
declaración np.array() . La función generará de forma automática la conversión a un objeto ndarray .
(16,)
int64
Para los casos n-dimensionales, np.array reconocerá de forma automática el shape de la lista.
[[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16], [6, 7, 3, 4, 12, 3, 4, 2, 3, 8,
2, 23, 35, 23, 12, 23]]
Numpy ofrece una serie de funciones para crear distintos tipos de ndarray
Indexing y Slicing en arrays vectoriales
Parte de las tareas más comunes es la indexación y creación de grupos. Para los arrays unidimensionales,
las operaciones son similares a la indexación de listas en Python nativo. Para indexar un dato, simplemente
se solicita a la lista que entregue el elemento guardado en la posición solicitada.
una_secuencia = np.arange(50)
una_secuencia[5]
Slicing hace referencia a la creación de subgrupos mediante la especificación de un rango inferior y superior
de las posiciones de los elementos. Algunas de las operaciones que se pueden realizar mediante el
operador Slice ( : ) son:
array([ 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21,
22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38,
39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49])
Si se asigna un valor a un subgrupo seleccionado mediante slicing, ésta se propagarán a la lista original
desde donde proviene la subselección
otra_secuencia = una_secuencia[10:18]
otra_secuencia[4] = 123456
print(otra_secuencia[4])
# El elemento de la posición 4 de subgrupo
# corresponde al elemento en la posición 14 en el arreglo original.
print(una_secuencia[14])
123456
123456
Arrays N-dimensionales
Vimos que con pandas es posible extraer un archivo csv ordenado en filas y columnas, y de éste generar
una representación de la tabla en un DataFrame . Lamentablemente numpy no presenta esta facilidad, por
lo que la representación de una matriz con filas y columnas se presenta como una lista de listas.
lista_de_listas = np.array([
[1, 2, 3, 4, 5],
[6, 7, 8, 9, 10],
[11, 12, 13, 14, 15]
])
lista_de_listas
array([[ 1, 2, 3, 4, 5],
[ 6, 7, 8, 9, 10],
[11, 12, 13, 14, 15]])
Dentro del objeto lista_de_listas , es posible ingresar a una de las sub listas mediante
array[posicion] .
lista_de_listas[2]
También es posible soicitar un elemento en una sublista específica mediante array[posicion_lista]
[posicion_elemento]
lista_de_listas[1][2]
lista_de_listas[1, 2]
Para aquellos casos donde las listas contengan 3 o más dimensiones, los índices se evalúan de forma
jerárquica desde mayor a menor.
lista_3d = np.array([
[[3, 5, 7, 9],
[8, 10, 12, 14],
[5, 15, 20, 25]],
[[6, 10, 14, 18],
[16, 20, 24, 28],
[10, 30, 40, 50]]
])
lista_3d
array([[[ 3, 5, 7, 9],
[ 8, 10, 12, 14],
[ 5, 15, 20, 25]],
lista_3d[0]
array([[ 3, 5, 7, 9],
[ 8, 10, 12, 14],
[ 5, 15, 20, 25]])
lista_3d[0][0]
array([3, 5, 7, 9])
lista_3d[0][0][0]
lista_de_listas[:2]
array([[ 1, 2, 3, 4, 5],
[ 6, 7, 8, 9, 10]])
En este caso, el operador slice asumió por defecto que se ha solicitado sólo los dos primeros elementos en
la primera dimensión. Así, se opera a nivel del primer contenedor.
El operador slice es muy flexbile, por lo que también es posible agregar más especificaciones. Ahora se
solicitarán las dos primeras listas, pero ignorando sus primeros elementos.
lista_de_listas[:2, 1:]
array([[ 2, 3, 4, 5],
[ 7, 8, 9, 10]])
Los operadores de slice se pueden utilizar en conjunto con índices simples y su resultado será el retorno de
los elementos solicitados con menor dimensión.
lista_de_listas[1,:2]
array([6, 7])
lista_de_listas[0,2]
Indexación booleana
Una de las estrategias más comunes para generar subgrupos de un conjunto de datos es mediante
operaciones booleanas. La flexibilidad de las expresiones booleanas permite clarificar y reducir la
complejidad de los criterios de búsqueda.
Por ejemplo, supongamos que tenemos una serie de elementos asociados a una lista de nombres.
array([[5, 3, 3, 6, 4, 2],
[6, 2, 5, 3, 3, 5],
[2, 3, 4, 4, 6, 2],
[1, 4, 6, 4, 1, 5],
[6, 2, 4, 2, 4, 1],
[1, 5, 1, 5, 5, 1],
[2, 2, 4, 4, 1, 1],
[2, 4, 3, 4, 6, 6],
[1, 2, 1, 4, 6, 5]])
Podemos preguntar por la membresía de algún elemento específico en el array con el operador ==
'Gonzalo' == nombres
array([ True, True, True, False, False, False, False, False, False])
A diferencia del comportamiento de una lista nativa de Python, si preguntamos por algún elemento
específico, numpy devolverá todas las evaluaciones para cada elemento
nombres == 'Fernanda'
Hasta el momento tenemos dos objetos separados, uno de nombres y otro de notas. Si queremos filtar
todas las notas de Magdalena, podemos implementar una indexación booleana dentro de las dimensiones
de nuestra matriz datos.
datos[nombres == 'Magdalena']
array([[1, 4, 6, 4, 1, 5]])
También podemos definir operaciones por vía negativa. En este caso, deseamos excluír las observaciones
que estén asociadas a David
datos[~(nombres == 'David')]
array([[5, 4, 3, 2, 3, 1],
[4, 2, 6, 3, 1, 6],
[6, 1, 2, 6, 3, 1],
[6, 1, 4, 6, 6, 4],
[1, 5, 2, 6, 6, 1],
[4, 5, 2, 3, 1, 5],
[6, 6, 5, 2, 4, 4],
[5, 5, 6, 5, 5, 3]])
También es posible sobreescribir todos los elementos que cumplan o no con una condición. En este caso,
queremos reemplazar por cero todas aquellas observaciones menores o igual a 4.
datos[datos <= 4] =0
datos
array([[5, 4, 3, 2, 3, 0],
[4, 2, 6, 3, 0, 6],
[6, 0, 2, 6, 3, 0],
[6, 0, 4, 6, 6, 4],
[0, 3, 3, 2, 6, 6],
[0, 5, 2, 6, 6, 0],
[4, 5, 2, 3, 0, 5],
[6, 6, 5, 2, 4, 4],
[5, 5, 6, 5, 5, 3]])
Finalmente, si queremos extraer dos condiciones, es bueno privilegiar operadores lógicos bitwise como &
o |
array([[5, 4, 3, 2, 3, 0],
[4, 2, 6, 3, 0, 6],
[6, 0, 2, 6, 3, 0],
[0, 3, 3, 2, 6, 6]])
Transposicionamiento de Arrays
Transpocionar permite generar subindexaciones de los datos sin la necesida de copiarlos
array([[ 0, 1, 2, 3, 4, 5],
[ 6, 7, 8, 9, 10, 11],
[12, 13, 14, 15, 16, 17],
[18, 19, 20, 21, 22, 23],
[24, 25, 26, 27, 28, 29]])
La matriz creada tiene 5 filas y 6 columnas. Esto se expresa en notación algebráica con . Para este
caso, array_3 = .
La transposición de una matriz significa pasar de las filas a las columnas y viceversa. Al aplicar el operador
T , nuestra matriz pasa a ser la siguiente:
array_3.T
Funciones Universales
numpy provee una serie de funciones universales, también conocidas en la jerga como ufuncs. Éstas
funciones buscan aplicar operaciones a todos los elementos contenidos en una estructura de datos
ndarray .
array_4 = np.arange(20)
np.sqrt(array_4)
np.exp(array_4)
Las funciones del ejemplo de arriba se conocen como funciones unarias, que reciben por defecto un
vector.
También existen las funciones binarias, que como lo indica su nombre, reciben dos vectores y devuelven
un vector con los resultados de la operación.
El mantra es siempre preferir trabajar con funciones orientadas al procesamiento de vectores por sobre
loops. Mediante esta forma, es más fácil abstraerse de los detalles y enfocarse en los substancial.
Operador ternario
numpy ofrece el operador ternario np.where que facilita la reconversión de elementos en base a si se
cumple una condición booleana o no.
Donde <condicion> hace referencia a un criterio formalizado en alguna expresión booleana que entregue
verdaderos o falsos. <valor_positivo> es el valor a asignar a aquellos elementos que satisfagan con la
condición. <valor_negativo> es el valor a asignar a aquellos elementos que no satisfagan la condición
array([ True, True, False, True, True, True, False, False, False,
True])
# Aquellas gatas cuya altura sea menor a la media de la lista, Y que además
# se encuentren en una posición correspondiente a True en la lista "tabby" (condición),
# serán gatas pequeñas y se almacenan como True (valor positivo),
# y las que no lo son se almacenan como False (valor negativo)
gatas_pequenas_tabby = np.where(
(altura_gatas < np.mean(altura_gatas)) & (tabby == True),
True,
False
)
gatas_pequenas_tabby
np.where no está limitado a asignar booleanos; Los valores positivos y negativos asignados pueden ser
números enteros, flotantes, cadenas, etc...
array_5 < 0
array([[False, False, True, False, False, True],
[ True, True, True, False, False, False],
[ True, False, True, False, False, False],
[False, False, False, True, False, False],
[False, True, False, True, True, False],
[False, False, True, True, True, True]])
array([[1, 1, 0, 1, 1, 0],
[0, 0, 0, 1, 1, 1],
[0, 1, 0, 1, 1, 1],
[1, 1, 1, 0, 1, 1],
[1, 0, 1, 0, 0, 1],
[1, 1, 0, 0, 0, 0]])